SERIAL read/write support for Witnesses and Mutation Tracking

Patch by Ariel Weisberg; Reviewed by Aleksey Yeschenko for CASSANDRA-20953
This commit is contained in:
Ariel Weisberg 2025-10-09 10:19:28 -07:00 committed by Blake Eggleston
parent 06e7dee9f8
commit b11122f23b
148 changed files with 9111 additions and 699 deletions

View File

@ -47,7 +47,8 @@ public enum Stage
MUTATION (true, "MutationStage", "request", DatabaseDescriptor::getConcurrentWriters, DatabaseDescriptor::setConcurrentWriters, Stage::multiThreadedLowSignalStage),
COUNTER_MUTATION (true, "CounterMutationStage", "request", DatabaseDescriptor::getConcurrentCounterWriters, DatabaseDescriptor::setConcurrentCounterWriters, Stage::multiThreadedLowSignalStage),
VIEW_MUTATION (true, "ViewMutationStage", "request", DatabaseDescriptor::getConcurrentViewWriters, DatabaseDescriptor::setConcurrentViewWriters, Stage::multiThreadedLowSignalStage),
ACCORD_MIGRATION (false, "AccordMigrationStage", "request", DatabaseDescriptor::getAccordConcurrentOps, DatabaseDescriptor::setConcurrentAccordOps, Stage::multiThreadedLowSignalStage),
TRACKED_CAS (true, "TrackedCasOpsStage", "request", DatabaseDescriptor::getConcurrentTrackedCasOps, DatabaseDescriptor::setConcurrentTrackedCasOps, Stage::multiThreadedLowSignalStage),
ACCORD_MIGRATION (false, "AccordMigrationStage", "request", DatabaseDescriptor::getAccordConcurrentOps, DatabaseDescriptor::setConcurrentAccordOps, Stage::multiThreadedLowSignalStage),
GOSSIP (true, "GossipStage", "internal", () -> 1, null, Stage::singleThreadedStage),
REQUEST_RESPONSE (false, "RequestResponseStage", "request", FBUtilities::getAvailableProcessors, null, Stage::multiThreadedLowSignalStage),
ANTI_ENTROPY (false, "AntiEntropyStage", "internal", () -> 1, null, Stage::singleThreadedStage),

View File

@ -213,6 +213,7 @@ public enum CassandraRelevantProperties
DIAGNOSTIC_SNAPSHOT_INTERVAL_NANOS("cassandra.diagnostic_snapshot_interval_nanos", "60000000000"),
DISABLE_AUTH_CACHES_REMOTE_CONFIGURATION("cassandra.disable_auth_caches_remote_configuration"),
/** properties to disable certain behaviours for testing */
DISABLE_CONSENSUS_REQUEST_FORWARDING("cassandra.disable_consensus_request_forwarding"),
DISABLE_GOSSIP_ENDPOINT_REMOVAL("cassandra.gossip.disable_endpoint_removal"),
DISABLE_PAXOS_AUTO_REPAIRS("cassandra.disable_paxos_auto_repairs"),
DISABLE_PAXOS_STATE_FLUSH("cassandra.disable_paxos_state_flush"),

View File

@ -208,6 +208,7 @@ public class Config
public int concurrent_accord_operations = 32;
public int concurrent_counter_writes = 32;
public int concurrent_materialized_view_writes = 32;
public int concurrent_tracked_cas_ops = 32;
public OptionaldPositiveInt available_processors = new OptionaldPositiveInt(CASSANDRA_AVAILABLE_PROCESSORS.getInt(OptionaldPositiveInt.UNDEFINED_VALUE));
public int memtable_flush_writers = 0;

View File

@ -2880,6 +2880,18 @@ public class DatabaseDescriptor
conf.concurrent_accord_operations = concurrent_operations;
}
public static int getConcurrentTrackedCasOps()
{
return conf.concurrent_tracked_cas_ops;
}
public static void setConcurrentTrackedCasOps(int concurrent_tracked_cas_ops)
{
if (concurrent_tracked_cas_ops < 0)
throw new IllegalArgumentException("Concurrent tracked CAS ops must be non-negative");
conf.concurrent_tracked_cas_ops = concurrent_tracked_cas_ops;
}
public static int getFlushWriters()
{
return conf.memtable_flush_writers;

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.cql3.constraints;
import org.apache.cassandra.exceptions.CassandraExceptionCode;
import org.apache.cassandra.exceptions.InvalidRequestException;
/**
@ -29,4 +30,10 @@ public class ConstraintViolationException extends InvalidRequestException
{
super(msg);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.CONSTRAINT_VIOLATION;
}
}

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.cql3.constraints;
import org.apache.cassandra.exceptions.CassandraExceptionCode;
import org.apache.cassandra.exceptions.InvalidRequestException;
/**
@ -29,4 +30,10 @@ public class InvalidConstraintDefinitionException extends InvalidRequestExceptio
{
super(msg);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.INVALID_CONSTRAINT_DEFINITION;
}
}

View File

@ -58,7 +58,6 @@ import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.IMutation;
import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts;
import org.apache.cassandra.db.RegularAndStaticColumns;
import org.apache.cassandra.db.Slice;
import org.apache.cassandra.db.Slices;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.partitions.PartitionUpdate;
@ -605,7 +604,7 @@ public class BatchStatement implements CQLStatement.CompositeCQLStatement
if (key == null)
{
key = statement.metadata().partitioner.decorateKey(pks.get(0));
casRequest = new CQL3CasRequest(statement.metadata(), key, conditionColumns, updatesRegularRows, updatesStaticRow, requestTime);
casRequest = new CQL3CasRequest(statement.metadata(), key, conditionColumns, updatesRegularRows, updatesStaticRow);
}
else if (!key.getKey().equals(pks.get(0)))
{
@ -626,11 +625,7 @@ public class BatchStatement implements CQLStatement.CompositeCQLStatement
if (slices.isEmpty())
continue;
for (Slice slice : slices)
{
casRequest.addRangeDeletion(slice, statement, statementOptions, timestamp, nowInSeconds);
}
casRequest.addWriteFragment(statement, statementOptions, state.getClientState(), nowInSeconds);
}
else
{
@ -644,7 +639,8 @@ public class BatchStatement implements CQLStatement.CompositeCQLStatement
else if (columnsWithConditions != null)
Iterables.addAll(columnsWithConditions, statement.getColumnsWithConditions());
}
casRequest.addRowUpdate(clustering, statement, statementOptions, timestamp, nowInSeconds);
casRequest.addWriteFragment(statement, statementOptions, state.getClientState(), nowInSeconds);
}
}

View File

@ -133,13 +133,16 @@ final class BatchUpdatesCollector implements UpdatesCollector
/**
* Returns a collection containing all the mutations.
*
*
* @param state state related to the client connection
*
* @param potentialTxnConflicts whether to allow potential transaction conflicts
* @param skipIndexValidation if true, skip index validation (used for CAS/transaction paths
* where validation happens later on the final materialized values)
*
* @return a collection containing all the mutations.
*/
@Override
public List<IMutation> toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts)
public List<IMutation> toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts, boolean skipIndexValidation)
{
List<IMutation> ms = new ArrayList<>();
for (Map<ByteBuffer, IMutationBuilder> ksMap : mutationBuilders.values())
@ -147,7 +150,8 @@ final class BatchUpdatesCollector implements UpdatesCollector
for (IMutationBuilder builder : ksMap.values())
{
IMutation mutation = builder.build(potentialTxnConflicts);
mutation.validateIndexedColumns(state);
if (!skipIndexValidation)
mutation.validateIndexedColumns(state);
mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE);
ms.add(mutation);
}

View File

@ -17,26 +17,24 @@
*/
package org.apache.cassandra.cql3.statements;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Objects;
import java.util.TreeMap;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.api.Update;
import accord.primitives.Keys;
import accord.primitives.Txn;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.UpdateParameters;
import org.apache.cassandra.cql3.conditions.ColumnCondition;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.Columns;
@ -45,28 +43,31 @@ import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts;
import org.apache.cassandra.db.RegularAndStaticColumns;
import org.apache.cassandra.db.SinglePartitionReadCommand;
import org.apache.cassandra.db.Slice;
import org.apache.cassandra.db.Slices;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter;
import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.filter.DataLimits;
import org.apache.cassandra.db.filter.RowFilter;
import org.apache.cassandra.db.marshal.TimeUUIDType;
import org.apache.cassandra.db.partitions.FilteredPartition;
import org.apache.cassandra.db.partitions.Partition;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.index.IndexRegistry;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableParams;
import org.apache.cassandra.service.CASRequest;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.PreserveTimestamp;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.accord.serializers.TableMetadatas;
import org.apache.cassandra.service.accord.serializers.TableMetadatasAndKeys;
import org.apache.cassandra.service.accord.serializers.Version;
import org.apache.cassandra.service.accord.txn.TxnCondition;
import org.apache.cassandra.service.accord.txn.TxnData;
import org.apache.cassandra.service.accord.txn.TxnDataKeyValue;
@ -80,7 +81,6 @@ import org.apache.cassandra.service.accord.txn.TxnWrite;
import org.apache.cassandra.service.paxos.Ballot;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.utils.TimeUUID;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.cassandra.service.StorageProxy.ConsensusAttemptResult;
@ -94,17 +94,13 @@ import static org.apache.cassandra.service.consensus.migration.ConsensusRequestR
/**
* Processed CAS conditions and update on potentially multiple rows of the same partition.
*/
public class CQL3CasRequest implements CASRequest
public class CQL3CasRequest
{
@SuppressWarnings("unused")
private static final Logger logger = LoggerFactory.getLogger(CQL3CasRequest.class);
public final TableMetadata metadata;
public final DecoratedKey key;
private final RegularAndStaticColumns conditionColumns;
private final boolean updatesRegularRows;
private final boolean updatesStaticRow;
private final Dispatcher.RequestTime requestTime;
private boolean hasExists; // whether we have an exist or if not exist condition
// Conditions on the static row. We keep it separate from 'conditions' as most things related to the static row are
@ -115,15 +111,13 @@ public class CQL3CasRequest implements CASRequest
// 2) this allows to detect when contradictory conditions are set (not exists with some other conditions on the same row)
private final TreeMap<Clustering<?>, RowCondition> conditions;
private final List<RowUpdate> updates = new ArrayList<>();
private final List<RangeDeletion> rangeDeletions = new ArrayList<>();
private final List<TxnWrite.Fragment> writeFragments = new ArrayList<>();
public CQL3CasRequest(TableMetadata metadata,
DecoratedKey key,
RegularAndStaticColumns conditionColumns,
boolean updatesRegularRows,
boolean updatesStaticRow,
Dispatcher.RequestTime requestTime)
boolean updatesStaticRow)
{
this.metadata = metadata;
this.key = key;
@ -131,23 +125,21 @@ public class CQL3CasRequest implements CASRequest
this.conditionColumns = conditionColumns;
this.updatesRegularRows = updatesRegularRows;
this.updatesStaticRow = updatesStaticRow;
this.requestTime = requestTime;
}
@Override
public Dispatcher.RequestTime requestTime()
{
return requestTime;
return Dispatcher.RequestTime.forImmediateExecution();
}
void addRowUpdate(Clustering<?> clustering, ModificationStatement stmt, QueryOptions options, long timestamp, long nowInSeconds)
{
updates.add(new RowUpdate(clustering, stmt, options, timestamp, nowInSeconds));
}
void addRangeDeletion(Slice slice, ModificationStatement stmt, QueryOptions options, long timestamp, long nowInSeconds)
void addWriteFragment(ModificationStatement stmt, QueryOptions options, ClientState clientState, long nowInSeconds)
{
rangeDeletions.add(new RangeDeletion(slice, stmt, options, timestamp, nowInSeconds));
// Create TxnWrite.Fragment directly using existing pattern
PartitionKey partitionKey = new PartitionKey(metadata.id, key);
List<TxnWrite.Fragment> fragments = stmt.forTxn().getTxnWriteFragment(
writeFragments.size(), clientState, options, partitionKey, nowInSeconds);
writeFragments.addAll(fragments);
}
public void addNotExist(Clustering<?> clustering) throws InvalidRequestException
@ -241,6 +233,9 @@ public class CQL3CasRequest implements CASRequest
return new RegularAndStaticColumns(statics, regulars);
}
/**
* The command to use to fetch the value to compare for the CAS.
*/
public SinglePartitionReadCommand readCommand(long nowInSec)
{
assert staticConditions != null || !conditions.isEmpty();
@ -289,111 +284,43 @@ public class CQL3CasRequest implements CASRequest
private RegularAndStaticColumns updatedColumns()
{
RegularAndStaticColumns.Builder builder = RegularAndStaticColumns.builder();
for (RowUpdate upd : updates)
builder.addAll(upd.stmt.updatedColumns());
for (TxnWrite.Fragment fragment : writeFragments)
{
builder.addAll(fragment.baseUpdate.columns());
if (!fragment.referenceOps.isEmpty())
{
// Add columns from reference operations
fragment.referenceOps.getStatics().forEach(op -> builder.add(op.receiver()));
fragment.referenceOps.getRegulars().forEach(op -> builder.add(op.receiver()));
}
}
return builder.build();
}
/**
* The updates to perform of a CAS success. The values fetched using the readFilter()
* are passed as argument.
*/
public PartitionUpdate makeUpdates(FilteredPartition current, ClientState clientState, Ballot ballot) throws InvalidRequestException
{
PartitionUpdate.Builder updateBuilder = new PartitionUpdate.Builder(metadata, key, updatedColumns(), conditions.size());
long timeUuidNanos = 0;
for (RowUpdate upd : updates)
timeUuidNanos = upd.applyUpdates(current, updateBuilder, clientState, ballot.msb(), timeUuidNanos);
for (RangeDeletion upd : rangeDeletions)
upd.applyUpdates(current, updateBuilder, clientState);
if (writeFragments.isEmpty())
return PartitionUpdate.emptyUpdate(metadata, key);
PartitionUpdate.Builder updateBuilder = new PartitionUpdate.Builder(
metadata, key, updatedColumns(), writeFragments.size());
// Create TxnData from read results
TxnDataKeyValue txnDataValue = new TxnDataKeyValue(current.rowIterator(false));
TxnData txnData = TxnData.of(txnDataName(CAS_READ), txnDataValue);
for (TxnWrite.Fragment fragment : writeFragments)
fragment.completeToBuilder(updateBuilder, txnData, ballot, current, clientState);
PartitionUpdate partitionUpdate = updateBuilder.build();
IndexRegistry.obtain(metadata).validate(partitionUpdate, clientState);
return partitionUpdate;
}
private static class CASUpdateParameters extends UpdateParameters
{
final long timeUuidMsb;
long timeUuidNanos;
public CASUpdateParameters(TableMetadata metadata, ClientState state, QueryOptions options, long timestamp, long nowInSec, int ttl, Map<DecoratedKey, Partition> prefetchedRows, long timeUuidMsb, long timeUuidNanos) throws InvalidRequestException
{
super(metadata, state, options, timestamp, nowInSec, ttl, prefetchedRows);
this.timeUuidMsb = timeUuidMsb;
this.timeUuidNanos = timeUuidNanos;
}
public byte[] nextTimeUUIDAsBytes()
{
return TimeUUID.toBytes(timeUuidMsb, TimeUUIDType.signedBytesToNativeLong(timeUuidNanos++));
}
}
/**
* Due to some operation on lists, we can't generate the update that a given Modification statement does before
* we get the values read by the initial read of Paxos. A RowUpdate thus just store the relevant information
* (include the statement iself) to generate those updates. We'll have multiple RowUpdate for a Batch, otherwise
* we'll have only one.
*/
private class RowUpdate
{
private final Clustering<?> clustering;
private final ModificationStatement stmt;
private final QueryOptions options;
private final long timestamp;
private final long nowInSeconds;
private RowUpdate(Clustering<?> clustering, ModificationStatement stmt, QueryOptions options, long timestamp, long nowInSeconds)
{
this.clustering = clustering;
this.stmt = stmt;
this.options = options;
this.timestamp = timestamp;
this.nowInSeconds = nowInSeconds;
}
long applyUpdates(FilteredPartition current, PartitionUpdate.Builder updateBuilder, ClientState state, long timeUuidMsb, long timeUuidNanos)
{
Map<DecoratedKey, Partition> map = stmt.requiresRead() ? Collections.singletonMap(key, current) : null;
CASUpdateParameters params =
new CASUpdateParameters(metadata, state, options, timestamp, nowInSeconds,
stmt.getTimeToLive(options), map, timeUuidMsb, timeUuidNanos);
stmt.addUpdateForKey(updateBuilder, clustering, params);
return params.timeUuidNanos;
}
}
private class RangeDeletion
{
private final Slice slice;
private final ModificationStatement stmt;
private final QueryOptions options;
private final long timestamp;
private final long nowInSeconds;
private RangeDeletion(Slice slice, ModificationStatement stmt, QueryOptions options, long timestamp, long nowInSeconds)
{
this.slice = slice;
this.stmt = stmt;
this.options = options;
this.timestamp = timestamp;
this.nowInSeconds = nowInSeconds;
}
void applyUpdates(FilteredPartition current, PartitionUpdate.Builder updateBuilder, ClientState state)
{
// No slice statements currently require a read, but this maintains consistency with RowUpdate, and future proofs us
Map<DecoratedKey, Partition> map = stmt.requiresRead() ? Collections.singletonMap(key, current) : null;
UpdateParameters params =
new UpdateParameters(metadata,
state,
options,
timestamp,
nowInSeconds,
stmt.getTimeToLive(options),
map);
stmt.addUpdateForKey(updateBuilder, slice, params);
}
}
private static abstract class RowCondition
{
public final Clustering<?> clustering;
@ -436,6 +363,21 @@ public class CQL3CasRequest implements CASRequest
TxnReference txnReference = TxnReference.row(txnDataName(CAS_READ));
return new TxnCondition.Exists(txnReference, TxnCondition.Kind.IS_NULL);
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NotExistCondition that = (NotExistCondition) o;
return Objects.equals(clustering, that.clustering);
}
@Override
public int hashCode()
{
return Objects.hash(clustering);
}
}
private static class ExistCondition extends RowCondition implements ToCQL
@ -461,6 +403,21 @@ public class CQL3CasRequest implements CASRequest
TxnReference txnReference = TxnReference.row(txnDataName(CAS_READ));
return new TxnCondition.Exists(txnReference, TxnCondition.Kind.IS_NOT_NULL);
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExistCondition that = (ExistCondition) o;
return Objects.equals(clustering, that.clustering);
}
@Override
public int hashCode()
{
return Objects.hash(clustering);
}
}
private static class ColumnsConditions extends RowCondition
@ -496,6 +453,22 @@ public class CQL3CasRequest implements CASRequest
{
return new TxnCondition.ColumnConditionsAdapter(clustering, conditions);
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ColumnsConditions that = (ColumnsConditions) o;
return Objects.equals(clustering, that.clustering) &&
Objects.equals(conditions, that.conditions);
}
@Override
public int hashCode()
{
return Objects.hash(clustering, conditions);
}
}
@Override
@ -505,6 +478,30 @@ public class CQL3CasRequest implements CASRequest
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CQL3CasRequest that = (CQL3CasRequest) o;
return updatesRegularRows == that.updatesRegularRows &&
updatesStaticRow == that.updatesStaticRow &&
hasExists == that.hasExists &&
Objects.equals(metadata.id, that.metadata.id) && // Compare table IDs instead of full metadata
Objects.equals(key, that.key) &&
Objects.equals(conditionColumns, that.conditionColumns) &&
Objects.equals(staticConditions, that.staticConditions) &&
Objects.equals(conditions, that.conditions) &&
Objects.equals(writeFragments, that.writeFragments);
}
@Override
public int hashCode()
{
return Objects.hash(metadata.id, key, conditionColumns, updatesRegularRows, updatesStaticRow,
hasExists, staticConditions, conditions, writeFragments);
}
public Txn toAccordTxn(ClusterMetadata cm, ConsistencyLevel consistencyLevel, ConsistencyLevel commitConsistencyLevel, ClientState clientState, long nowInSecs)
{
SinglePartitionReadCommand readCommand = readCommand(nowInSecs);
@ -531,7 +528,7 @@ public class CQL3CasRequest implements CASRequest
TableParams tableParams = tableMetadata.params;
commitConsistencyLevel = tableParams.transactionalMode.commitCLForMode(tableParams.transactionalMigrationFrom, commitConsistencyLevel, cm, tableMetadata.id, key.getToken());
// CAS requires using the new txn timestamp to correctly linearize some kinds of updates
return new TxnUpdate(tables, createWriteFragments(clientState), createCondition(), commitConsistencyLevel, PreserveTimestamp.no);
return new TxnUpdate(tables, writeFragments, createCondition(), commitConsistencyLevel, PreserveTimestamp.no);
}
private TxnCondition createCondition()
@ -548,30 +545,6 @@ public class CQL3CasRequest implements CASRequest
return conditions.size() == 1 ? txnConditions.get(0) : new TxnCondition.BooleanGroup(TxnCondition.Kind.AND, txnConditions);
}
private List<TxnWrite.Fragment> createWriteFragments(ClientState state)
{
PartitionKey partitionKey = new PartitionKey(metadata.id, key);
List<TxnWrite.Fragment> fragments = new ArrayList<>();
int idx = 0;
for (RowUpdate update : updates)
{
// Some operations may need to migrate to run in the transaction, so need to call forTxn to make sure this
// happens.
// see CASSANDRA-18337
ModificationStatement modification = update.stmt.forTxn();
QueryOptions options = update.options;
fragments.addAll(modification.getTxnWriteFragment(idx++, state, options, partitionKey));
}
for (RangeDeletion rangeDeletion : rangeDeletions)
{
ModificationStatement modification = rangeDeletion.stmt;
QueryOptions options = rangeDeletion.options;
fragments.addAll(modification.getTxnWriteFragment(idx++, state, options, partitionKey));
}
return fragments;
}
@Override
public ConsensusAttemptResult toCasResult(TxnResult txnResult)
{
if (txnResult.kind() == retry_new_protocol)
@ -581,4 +554,197 @@ public class CQL3CasRequest implements CASRequest
TxnDataKeyValue partition = (TxnDataKeyValue)txnData.get(txnDataName(CAS_READ));
return casResult(partition != null ? partition.rowIterator(false) : null);
}
public static final Serializer serializer = new Serializer();
/**
* IVersionedSerializer for CQL3CasRequest to enable CAS forwarding between coordinators.
*
*/
public static class Serializer implements IVersionedSerializer<CQL3CasRequest>
{
private static final int UPDATES_REGULAR_ROWS = 0x01;
private static final int UPDATES_STATIC_ROW = 0x02;
private static final int HAS_EXISTS = 0x04;
private static final byte CONDITION_NULL = 0;
private static final byte CONDITION_NOT_EXIST = 1;
private static final byte CONDITION_EXIST = 2;
private static final byte CONDITION_COLUMNS = 3;
@Override
public void serialize(CQL3CasRequest request, DataOutputPlus out, int version) throws IOException
{
int flags = (request.updatesRegularRows ? UPDATES_REGULAR_ROWS : 0)
| (request.updatesStaticRow ? UPDATES_STATIC_ROW : 0)
| (request.hasExists ? HAS_EXISTS : 0)
;
out.write(flags);
request.metadata.id.serializeCompact(out);
DecoratedKey.serializer.serialize(request.key, out, version);
Columns.serializer.serialize(request.conditionColumns.statics, out);
Columns.serializer.serialize(request.conditionColumns.regulars, out);
serializeRowCondition(request.staticConditions, out, version);
out.writeUnsignedVInt32(request.conditions.size());
for (Map.Entry<Clustering<?>, RowCondition> entry : request.conditions.entrySet())
{
Clustering.serializer.serialize(entry.getKey(), out, version, request.metadata.comparator.subtypes());
serializeRowCondition(entry.getValue(), out, version);
}
out.writeUnsignedVInt32(request.writeFragments.size());
TableMetadatas tableMetadatas = TableMetadatas.of(request.metadata);
for (TxnWrite.Fragment fragment : request.writeFragments)
TxnWrite.Fragment.serializer.serialize(fragment, tableMetadatas, out, Version.findBestMatchForMessagingVersion(version));
}
@Override
public CQL3CasRequest deserialize(DataInputPlus in, int version) throws IOException
{
int flags = in.readUnsignedByte();
boolean updatesRegularRows = (flags & UPDATES_REGULAR_ROWS) != 0;
boolean updatesStaticRow = (flags & UPDATES_STATIC_ROW) != 0;
boolean hasExists = (flags & HAS_EXISTS) != 0;
TableId tableId = TableId.deserializeCompact(in);
TableMetadata metadata = Schema.instance.getTableMetadata(tableId);
if (metadata == null)
throw new IOException("Unknown table ID in CQL3CasRequest deserialization: " + tableId);
DecoratedKey key = (DecoratedKey) DecoratedKey.serializer.deserialize(in, version);
Columns statics = Columns.serializer.deserialize(in, metadata);
Columns regulars = Columns.serializer.deserialize(in, metadata);
RegularAndStaticColumns conditionColumns = new RegularAndStaticColumns(statics, regulars);
CQL3CasRequest request = new CQL3CasRequest(metadata, key, conditionColumns, updatesRegularRows, updatesStaticRow);
request.hasExists = hasExists;
request.staticConditions = deserializeRowCondition(in, version, metadata, Clustering.STATIC_CLUSTERING);
int conditionsCount = in.readUnsignedVInt32();
for (int i = 0; i < conditionsCount; i++)
{
Clustering<?> clustering = Clustering.serializer.deserialize(in, version, metadata.comparator.subtypes());
RowCondition condition = deserializeRowCondition(in, version, metadata, clustering);
request.conditions.put(clustering, condition);
}
int fragmentCount = in.readUnsignedVInt32();
TableMetadatas tableMetadatas = TableMetadatas.of(metadata);
for (int i = 0; i < fragmentCount; i++)
{
PartitionKey partitionKey = new PartitionKey(metadata.id, request.key);
TxnWrite.Fragment fragment = TxnWrite.Fragment.serializer.deserialize(partitionKey, tableMetadatas, in, Version.findBestMatchForMessagingVersion(version));
request.writeFragments.add(fragment);
}
return request;
}
@Override
public long serializedSize(CQL3CasRequest request, int version)
{
// Flags byte
long size = 1;
size += request.metadata.id.serializedCompactSize();
size += DecoratedKey.serializer.serializedSize(request.key, version);
size += Columns.serializer.serializedSize(request.conditionColumns.statics);
size += Columns.serializer.serializedSize(request.conditionColumns.regulars);
size += rowConditionSize(request.staticConditions, version);
size += TypeSizes.sizeofUnsignedVInt(request.conditions.size());
for (Map.Entry<Clustering<?>, RowCondition> entry : request.conditions.entrySet())
{
size += Clustering.serializer.serializedSize(entry.getKey(), version, request.metadata.comparator.subtypes());
size += rowConditionSize(entry.getValue(), version);
}
size += TypeSizes.sizeofUnsignedVInt(request.writeFragments.size());
for (TxnWrite.Fragment fragment : request.writeFragments)
size += TxnWrite.Fragment.serializer.serializedSize(fragment, TableMetadatas.of(request.metadata), Version.findBestMatchForMessagingVersion(version));
return size;
}
private void serializeRowCondition(RowCondition condition, DataOutputPlus out, int version) throws IOException
{
if (condition == null)
{
out.writeByte(CONDITION_NULL);
}
else if (condition instanceof NotExistCondition)
{
out.writeByte(CONDITION_NOT_EXIST);
// Don't serialize clustering here - it's already serialized in the conditions map
}
else if (condition instanceof ExistCondition)
{
out.writeByte(CONDITION_EXIST);
// Don't serialize clustering here - it's already serialized in the conditions map
}
else if (condition instanceof ColumnsConditions)
{
out.writeByte(CONDITION_COLUMNS);
ColumnsConditions cc = (ColumnsConditions) condition;
out.writeUnsignedVInt32(cc.conditions.size());
// Serialize each ColumnCondition.Bound using adapted pattern
for (ColumnCondition.Bound bound : cc.conditions)
ColumnCondition.Bound.serializer.serialize(bound, TableMetadatas.of(bound.table), out);
}
else
{
throw new IOException("Unknown RowCondition type: " + condition.getClass());
}
}
private RowCondition deserializeRowCondition(DataInputPlus in, int version, TableMetadata metadata, Clustering<?> clustering) throws IOException
{
byte type = in.readByte();
switch (type)
{
case CONDITION_NULL:
return null;
case CONDITION_NOT_EXIST:
return new NotExistCondition(clustering);
case CONDITION_EXIST:
return new ExistCondition(clustering);
case CONDITION_COLUMNS:
int conditionsCount = in.readUnsignedVInt32();
ColumnsConditions columnsConditions = new ColumnsConditions(clustering);
// Deserialize each ColumnCondition.Bound
for (int i = 0; i < conditionsCount; i++)
{
ColumnCondition.Bound bound = ColumnCondition.Bound.serializer.deserialize(TableMetadatas.of(metadata), in);
columnsConditions.conditions.add(bound);
}
return columnsConditions;
default:
throw new IOException("Unknown RowCondition type: " + type);
}
}
private long rowConditionSize(RowCondition condition, int version)
{
long size = 1; // type byte
if (condition instanceof ColumnsConditions)
{
ColumnsConditions cc = (ColumnsConditions) condition;
size += TypeSizes.sizeofUnsignedVInt(cc.conditions.size());
// Calculate size for each ColumnCondition.Bound
for (ColumnCondition.Bound bound : cc.conditions)
size += ColumnCondition.Bound.serializer.serializedSize(bound, TableMetadatas.of(bound.table));
}
return size;
}
}
}

View File

@ -729,11 +729,11 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
type.isUpdate()? "updates" : "deletions");
Clustering<?> clustering = Iterables.getOnlyElement(createClustering(options, clientState));
CQL3CasRequest request = new CQL3CasRequest(metadata(), key, conditionColumns(), updatesRegularRows(), updatesStaticRow(), requestTime);
CQL3CasRequest request = new CQL3CasRequest(metadata(), key, conditionColumns(), updatesRegularRows(), updatesStaticRow());
addConditions(clustering, request, options);
request.addRowUpdate(clustering, this, options, timestamp, nowInSeconds);
request.addWriteFragment(this, options, clientState, nowInSeconds);
return request;
}
@ -906,6 +906,47 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
return null;
}
/**
* Convert statement into a list of mutations to apply on the server
*
* @param state the client state
* @param options value for prepared statement markers
* @param local if true, any requests (for collections) performed by getMutation should be done locally only.
* @param timestamp the current timestamp in microseconds to use if no timestamp is user provided.
* @param nowInSeconds the current time in seconds
* @param requestTime the request time
* @param skipIndexValidation if true, skip index validation (used for CAS/transaction paths
* where validation happens later on the final materialized values)
*
* @return list of the mutations
*/
public List<? extends IMutation> getMutations(ClientState state,
QueryOptions options,
boolean local,
long timestamp,
long nowInSeconds,
Dispatcher.RequestTime requestTime,
boolean skipIndexValidation)
{
List<ByteBuffer> keys = buildPartitionKeyNames(options, state);
if (keys.size() == 1)
{
SingleTableSinglePartitionUpdatesCollector collector = new SingleTableSinglePartitionUpdatesCollector(metadata, updatedColumns);
addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime);
// local means this is test or internal things that are bypassing distributed system modification/checks
return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW, skipIndexValidation);
}
else
{
HashMultiset<ByteBuffer> perPartitionKeyCounts = HashMultiset.create(keys);
SingleTableUpdatesCollector collector = new SingleTableUpdatesCollector(metadata, updatedColumns, perPartitionKeyCounts);
addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime);
// local means this is test or internal things that are bypassing distributed system modification/checks
return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW, skipIndexValidation);
}
}
/**
* Convert statement into a list of mutations to apply on the server
*
@ -923,28 +964,15 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
long nowInSeconds,
Dispatcher.RequestTime requestTime)
{
List<ByteBuffer> keys = buildPartitionKeyNames(options, state);
if (keys.size() == 1)
{
SingleTableSinglePartitionUpdatesCollector collector = new SingleTableSinglePartitionUpdatesCollector(metadata, updatedColumns);
addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime);
// local means this is test or internal things that are bypassing distributed system modification/checks
return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW);
}
else
{
HashMultiset<ByteBuffer> perPartitionKeyCounts = HashMultiset.create(keys);
SingleTableUpdatesCollector collector = new SingleTableUpdatesCollector(metadata, updatedColumns, perPartitionKeyCounts);
addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime);
// local means this is test or internal things that are bypassing distributed system modification/checks
return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW);
}
return getMutations(state, options, local, timestamp, nowInSeconds, requestTime, false);
}
public List<PartitionUpdate> getTxnUpdate(ClientState state, QueryOptions options)
public List<PartitionUpdate> getTxnUpdate(ClientState state, QueryOptions options, long nowInSeconds)
{
List<? extends IMutation> mutations = getMutations(state, options, false, 0, 0, new Dispatcher.RequestTime(0, 0));
// Skip index validation here because validation happens later on the final materialized values
// in CQL3CasRequest.makeUpdates()
List<? extends IMutation> mutations = getMutations(state, options, false, 0, nowInSeconds, new Dispatcher.RequestTime(0, 0), true);
// TODO: Temporary fix for CASSANDRA-20079
if (mutations.isEmpty())
return Collections.emptyList();
List<PartitionUpdate> updates = new ArrayList<>(mutations.size());
@ -1004,22 +1032,22 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
return operations.allSubstitutions();
}
public List<TxnWrite.Fragment> getTxnWriteFragment(int index, ClientState state, QueryOptions options, PartitionKey partitionKey)
public List<TxnWrite.Fragment> getTxnWriteFragment(int index, ClientState state, QueryOptions options, PartitionKey partitionKey, long nowInSeconds)
{
return getTxnWriteFragment(index, state, options, baseUpdate -> {
Invariants.require(baseUpdate.partitionKey().equals(partitionKey.partitionKey()), "PartitionUpdate generated a partition key different than the one expected");
return partitionKey;
});
}, nowInSeconds);
}
public List<TxnWrite.Fragment> getTxnWriteFragment(int index, ClientState state, QueryOptions options, KeyCollector keyCollector)
public List<TxnWrite.Fragment> getTxnWriteFragment(int index, ClientState state, QueryOptions options, KeyCollector keyCollector, long nowInSeconds)
{
return getTxnWriteFragment(index, state, options, baseUpdate -> keyCollector.collect(baseUpdate.metadata(), baseUpdate.partitionKey()));
return getTxnWriteFragment(index, state, options, baseUpdate -> keyCollector.collect(baseUpdate.metadata(), baseUpdate.partitionKey()), nowInSeconds);
}
private List<TxnWrite.Fragment> getTxnWriteFragment(int index, ClientState state, QueryOptions options, java.util.function.Function<PartitionUpdate, PartitionKey> keyCollector)
private List<TxnWrite.Fragment> getTxnWriteFragment(int index, ClientState state, QueryOptions options, java.util.function.Function<PartitionUpdate, PartitionKey> keyCollector, long nowInSeconds)
{
List<PartitionUpdate> baseUpdates = getTxnUpdate(state, options);
List<PartitionUpdate> baseUpdates = getTxnUpdate(state, options, nowInSeconds);
TxnReferenceOperations referenceOps = getTxnReferenceOps(options, state);
long timestamp = attrs.isTimestampSet() ? attrs.getTimestamp(TxnWrite.NO_TIMESTAMP, options) : TxnWrite.NO_TIMESTAMP;
if (baseUpdates.size() == 1)

View File

@ -80,16 +80,16 @@ final class SingleTableSinglePartitionUpdatesCollector implements UpdatesCollect
* Returns a collection containing all the mutations.
*/
@Override
public List<IMutation> toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts)
public List<IMutation> toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts, boolean skipIndexValidation)
{
// it is possible that a modification statement does not create any mutations
// for example: DELETE FROM some_table WHERE part_key = 1 AND clust_key < 3 AND clust_key > 5
if (builder == null)
return Collections.emptyList();
return Collections.singletonList(createMutation(state, builder, potentialTxnConflicts));
return Collections.singletonList(createMutation(state, builder, potentialTxnConflicts, skipIndexValidation));
}
private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, PotentialTxnConflicts potentialTxnConflicts)
private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, PotentialTxnConflicts potentialTxnConflicts, boolean skipIndexValidation)
{
IMutation mutation;
@ -100,7 +100,8 @@ final class SingleTableSinglePartitionUpdatesCollector implements UpdatesCollect
else
mutation = new Mutation(MutationId.fixme(), builder.build(), potentialTxnConflicts);
mutation.validateIndexedColumns(state);
if (!skipIndexValidation)
mutation.validateIndexedColumns(state);
mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE);
return mutation;
}

View File

@ -97,24 +97,24 @@ final class SingleTableUpdatesCollector implements UpdatesCollector
* @return a collection containing all the mutations.
*/
@Override
public List<IMutation> toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts)
public List<IMutation> toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts, boolean skipIndexValidation)
{
if (puBuilders.size() == 1)
{
PartitionUpdate.Builder builder = puBuilders.values().iterator().next();
return Collections.singletonList(createMutation(state, builder, potentialTxnConflicts));
return Collections.singletonList(createMutation(state, builder, potentialTxnConflicts, skipIndexValidation));
}
List<IMutation> ms = new ArrayList<>(puBuilders.size());
for (PartitionUpdate.Builder builder : puBuilders.values())
{
IMutation mutation = createMutation(state, builder, potentialTxnConflicts);
IMutation mutation = createMutation(state, builder, potentialTxnConflicts, skipIndexValidation);
ms.add(mutation);
}
return ms;
}
private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, PotentialTxnConflicts potentialTxnConflicts)
private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, PotentialTxnConflicts potentialTxnConflicts, boolean skipIndexValidation)
{
IMutation mutation;
@ -125,7 +125,8 @@ final class SingleTableUpdatesCollector implements UpdatesCollector
else
mutation = new Mutation(MutationId.fixme(), builder.build(), potentialTxnConflicts);
mutation.validateIndexedColumns(state);
if (!skipIndexValidation)
mutation.validateIndexedColumns(state);
mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE);
return mutation;
}

View File

@ -355,14 +355,14 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement,
return new Keys(keySet);
}
List<TxnWrite.Fragment> createWriteFragments(ClientState state, QueryOptions options, Map<Integer, NamedSelect> autoReads, TableMetadatasAndKeys.KeyCollector keyCollector)
List<TxnWrite.Fragment> createWriteFragments(ClientState state, QueryOptions options, Map<Integer, NamedSelect> autoReads, TableMetadatasAndKeys.KeyCollector keyCollector, long nowInSeconds)
{
List<TxnWrite.Fragment> fragments = new ArrayList<>(updates.size());
int idx = 0;
for (ModificationStatement modification : updates)
{
minEpoch = Math.max(minEpoch, modification.metadata().epoch.getEpoch());
fragments.addAll(modification.getTxnWriteFragment(idx, state, options, keyCollector));
fragments.addAll(modification.getTxnWriteFragment(idx, state, options, keyCollector, nowInSeconds));
if (modification.allReferenceOperations().stream().anyMatch(ReferenceOperation::requiresRead))
{
@ -477,7 +477,8 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement,
else
{
Int2ObjectHashMap<NamedSelect> autoReads = new Int2ObjectHashMap<>();
List<TxnWrite.Fragment> writeFragments = createWriteFragments(state, options, autoReads, keyCollector);
long nowInSeconds = options.getNowInSec(FBUtilities.nowInSeconds());
List<TxnWrite.Fragment> writeFragments = createWriteFragments(state, options, autoReads, keyCollector, nowInSeconds);
List<TxnNamedRead> reads = createNamedReads(options, autoReads, keyCollector);
if (writeFragments.isEmpty()) // ModificationStatement yield no Mutation (DELETE WHERE pk=0 AND c < 0 AND c > 0 -- matches no keys; so has no mutation)
{

View File

@ -31,5 +31,20 @@ import org.apache.cassandra.service.ClientState;
public interface UpdatesCollector
{
PartitionUpdate.Builder getPartitionUpdateBuilder(TableMetadata metadata, DecoratedKey dk, ConsistencyLevel consistency);
List<IMutation> toMutations(ClientState state, PotentialTxnConflicts allowPotentialTxnConflicts);
/**
* Builds mutations from the collected updates.
*
* @param state the client state
* @param allowPotentialTxnConflicts whether to allow potential transaction conflicts
* @param skipIndexValidation if true, skip index validation (used for CAS/transaction paths
* where validation happens later on the final materialized values)
* @return the list of mutations
*/
List<IMutation> toMutations(ClientState state, PotentialTxnConflicts allowPotentialTxnConflicts, boolean skipIndexValidation);
default List<IMutation> toMutations(ClientState state, PotentialTxnConflicts allowPotentialTxnConflicts)
{
return toMutations(state, allowPotentialTxnConflicts, false);
}
}

View File

@ -174,9 +174,6 @@ public final class CreateIndexStatement extends AlterSchemaStatement
if (table.isView())
throw ire(MATERIALIZED_VIEWS_NOT_SUPPORTED);
if (keyspace.replicationStrategy.hasTransientReplicas())
throw new InvalidRequestException(TRANSIENTLY_REPLICATED_KEYSPACE_NOT_SUPPORTED);
// guardrails to limit number of secondary indexes per table.
Guardrails.secondaryIndexesPerTable.guard(table.indexes.size() + 1,
Strings.isNullOrEmpty(indexName)

View File

@ -0,0 +1,120 @@
/*
* 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.db;
import java.io.IOException;
import com.google.common.base.Preconditions;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.reads.tracked.TrackedRead.DataRequest;
import org.apache.cassandra.service.reads.tracked.TrackedRead.SummaryRequest;
import static org.apache.cassandra.db.ReadKind.UNTRACKED;
/**
* Interface for read command that allows it be serialized and embedded in another message. Used in Paxos
* to provide a common base class to serialize for tracked and untracked reads. Tracked reads contain
* additional information needed to execute the read beyond the read command itself so an additional interface
* is needed.
*/
public interface EmbeddableSinglePartitionReadCommand
{
ReadKind kind();
default boolean isTracked()
{
return kind().isTracked();
}
TableMetadata metadata();
DecoratedKey partitionKey();
IVersionedSerializer<EmbeddableSinglePartitionReadCommand> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(EmbeddableSinglePartitionReadCommand command, DataOutputPlus out, int version) throws IOException
{
if (version >= MessagingService.VERSION_61)
ReadKind.serializer.serialize(command.kind(), out);
else
Preconditions.checkArgument(command.kind() == UNTRACKED);
switch (command.kind())
{
case UNTRACKED:
ReadCommand.serializer.serialize((ReadCommand) command, out, version);
break;
case TRACKED_DATA:
DataRequest.serializer.serialize((DataRequest) command, out, version);
break;
case TRACKED_SUMMARY:
SummaryRequest.serializer.serialize((SummaryRequest) command, out, version);
break;
default:
throw new IllegalStateException("Unhandled kind: " + command.kind());
}
}
@Override
public EmbeddableSinglePartitionReadCommand deserialize(DataInputPlus in, int version) throws IOException
{
ReadKind kind = version >= MessagingService.VERSION_61 ? ReadKind.serializer.deserialize(in) : UNTRACKED;
switch (kind)
{
case UNTRACKED:
return (SinglePartitionReadCommand)ReadCommand.serializer.deserialize(in, version);
case TRACKED_DATA:
return DataRequest.serializer.deserialize(in, version);
case TRACKED_SUMMARY:
return SummaryRequest.serializer.deserialize(in, version);
default:
throw new IllegalStateException("Unhandled kind: " + kind);
}
}
@Override
public long serializedSize(EmbeddableSinglePartitionReadCommand command, int version)
{
long size = 0;
if (version >= MessagingService.VERSION_61)
size += ReadKind.serializer.serializedSize(command.kind());
else
Preconditions.checkArgument(command.kind() == UNTRACKED);
switch (command.kind())
{
case UNTRACKED:
return size + ReadCommand.serializer.serializedSize((ReadCommand) command, version);
case TRACKED_DATA:
return size + DataRequest.serializer.serializedSize((DataRequest) command, version);
case TRACKED_SUMMARY:
return size + SummaryRequest.serializer.serializedSize((SummaryRequest) command, version);
default:
throw new IllegalStateException("Unhandled kind: " + command.kind());
}
}
};
}

View File

@ -0,0 +1,104 @@
/*
* 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.db;
import java.io.IOException;
import com.google.common.base.Preconditions;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.service.reads.tracked.TrackedDataResponse;
import org.apache.cassandra.service.reads.tracked.TrackedSummaryResponse;
import static org.apache.cassandra.db.ReadKind.UNTRACKED;
public interface IReadResponse
{
ReadKind kind();
IVersionedSerializer<IReadResponse> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(IReadResponse response, DataOutputPlus out, int version) throws IOException
{
if (version >= MessagingService.VERSION_61)
ReadKind.serializer.serialize(response.kind(), out);
else
Preconditions.checkArgument(response.kind() == UNTRACKED);
switch (response.kind())
{
case UNTRACKED:
ReadResponse.serializer.serialize((ReadResponse) response, out, version);
break;
case TRACKED_DATA:
TrackedDataResponse.serializer.serialize((TrackedDataResponse) response, out, version);
break;
case TRACKED_SUMMARY:
TrackedSummaryResponse.serializer.serialize((TrackedSummaryResponse) response, out, version);
break;
default:
throw new IllegalStateException("Unhandled kind: " + response.kind());
}
}
@Override
public IReadResponse deserialize(DataInputPlus in, int version) throws IOException
{
ReadKind kind = version >= MessagingService.VERSION_61 ? ReadKind.serializer.deserialize(in) : UNTRACKED;
switch (kind)
{
case UNTRACKED:
return ReadResponse.serializer.deserialize(in, version);
case TRACKED_DATA:
return TrackedDataResponse.serializer.deserialize(in, version);
case TRACKED_SUMMARY:
return TrackedSummaryResponse.serializer.deserialize(in, version);
default:
throw new IllegalStateException("Unhandled kind: " + kind);
}
}
@Override
public long serializedSize(IReadResponse response, int version)
{
long size = 0;
if (version >= MessagingService.VERSION_61)
size += ReadKind.serializer.serializedSize(response.kind());
else
Preconditions.checkArgument(response.kind() == UNTRACKED);
switch (response.kind())
{
case UNTRACKED:
return size + ReadResponse.serializer.serializedSize((ReadResponse) response, version);
case TRACKED_DATA:
return size + TrackedDataResponse.serializer.serializedSize((TrackedDataResponse) response, version);
case TRACKED_SUMMARY:
return size + TrackedSummaryResponse.serializer.serializedSize((TrackedSummaryResponse) response, version);
default:
throw new IllegalStateException("Unhandled kind: " + response.kind());
}
}
};
}

View File

@ -17,6 +17,7 @@
*/
package org.apache.cassandra.db;
import org.apache.cassandra.exceptions.CassandraExceptionCode;
import org.apache.cassandra.exceptions.InvalidRequestException;
public class KeyspaceNotDefinedException extends InvalidRequestException
@ -25,4 +26,10 @@ public class KeyspaceNotDefinedException extends InvalidRequestException
{
super(why);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.KEYSPACE_NOT_DEFINED;
}
}

View File

@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
@ -58,6 +59,7 @@ import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.AbstractWriteResponseHandler;
import org.apache.cassandra.service.paxos.Commit.Commitable;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.concurrent.Future;
@ -69,7 +71,7 @@ import static org.apache.cassandra.net.MessagingService.VERSION_60;
import static org.apache.cassandra.net.MessagingService.VERSION_61;
import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
public class Mutation implements IMutation, Supplier<Mutation>
public class Mutation implements IMutation, Supplier<Mutation>, Commitable
{
public static final MutationSerializer serializer = new MutationSerializer();
public static final int ALLOW_POTENTIAL_TRANSACTION_CONFLICTS = 0x01;
@ -215,6 +217,13 @@ public class Mutation implements IMutation, Supplier<Mutation>
return modifications.values();
}
public @Nonnull PartitionUpdate getOnlyUpdate()
{
checkState(modifications.size() == 1, "Should only have one PartitionUpdate");
//noinspection ConstantConditions
return modifications().values().iterator().next();
}
public long getApproxCreatedAtNanos()
{
return approxCreatedAtNanos;
@ -468,6 +477,12 @@ public class Mutation implements IMutation, Supplier<Mutation>
return new SimpleBuilders.MutationBuilder(mutationId, keyspaceName, partitionKey);
}
@Override
public CommitableKind commitableKind()
{
return CommitableKind.MUTATION;
}
/**
* Interface for building mutations geared towards human.
* <p>
@ -685,10 +700,10 @@ public class Mutation implements IMutation, Supplier<Mutation>
public TableId deserializeTableId(DataInputBuffer in, int version, DeserializationHelper.Flag flag) throws IOException
{
if (version >= VERSION_51)
if (version >= VERSION_60)
in.skipBytes(1); // flags
if (version >= VERSION_52)
if (version >= VERSION_61)
MutationId.serializer.skip(in, version);
int size = in.readUnsignedVInt32();

View File

@ -21,11 +21,15 @@ package org.apache.cassandra.db;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import java.io.IOException;
import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.exceptions.CassandraExceptionCode;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import static org.apache.cassandra.db.IMutation.MAX_MUTATION_SIZE;
@ -41,6 +45,13 @@ public class MutationExceededMaxSizeException extends InvalidRequestException
this.mutationSize = totalSize;
}
// Constructor for deserialization
public MutationExceededMaxSizeException(String message, long mutationSize)
{
super(message);
this.mutationSize = mutationSize;
}
private static String prepareMessage(IMutation mutation, int version, long totalSize)
{
List<String> topPartitions = mutation.getPartitionUpdates().stream()
@ -87,4 +98,28 @@ public class MutationExceededMaxSizeException extends InvalidRequestException
return stringBuilder.toString();
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.MUTATION_EXCEEDED_MAX_SIZE;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
out.writeUnsignedVInt(mutationSize);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return TypeSizes.sizeofUnsignedVInt(mutationSize);
}
public static MutationExceededMaxSizeException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
long mutationSize = in.readUnsignedVInt();
return new MutationExceededMaxSizeException(message, mutationSize);
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.db;
import java.io.IOException;
import org.apache.cassandra.io.UnversionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
public enum ReadKind
{
UNTRACKED (0),
TRACKED_DATA (1),
TRACKED_SUMMARY (2);
public final int id;
ReadKind(int id)
{
this.id = id;
}
public boolean isTracked()
{
return this != UNTRACKED;
}
private static final ReadKind[] idToKindMapping;
static
{
int maxId = -1;
for (ReadKind kind : ReadKind.values())
maxId = Math.max(maxId, kind.id);
idToKindMapping = new ReadKind[maxId + 1];
for (ReadKind kind : ReadKind.values())
idToKindMapping[kind.id] = kind;
}
public static ReadKind fromId(int id)
{
if (id < 0 || id >= idToKindMapping.length || idToKindMapping[id] == null)
throw new IllegalArgumentException("Unknown Kind id: " + id);
return idToKindMapping[id];
}
public static final UnversionedSerializer<ReadKind> serializer = new UnversionedSerializer<>()
{
@Override
public void serialize(ReadKind kind, DataOutputPlus out) throws IOException
{
out.writeByte(kind.id);
}
@Override
public ReadKind deserialize(DataInputPlus in) throws IOException
{
return fromId(in.readUnsignedByte());
}
@Override
public long serializedSize(ReadKind kind)
{
return TypeSizes.BYTE_SIZE;
}
};
}

View File

@ -39,9 +39,10 @@ import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
import static org.apache.cassandra.db.ReadKind.UNTRACKED;
import static org.apache.cassandra.db.RepairedDataInfo.NO_OP_REPAIRED_DATA_INFO;
public abstract class ReadResponse
public abstract class ReadResponse implements IReadResponse
{
// Serializer for single partition read response
public static final IVersionedSerializer<ReadResponse> serializer = new Serializer();
@ -115,6 +116,12 @@ public abstract class ReadResponse
key, ByteBufferUtil.bytesToHex(repairedDataDigest()), isRepairedDigestConclusive());
}
@Override
public ReadKind kind()
{
return UNTRACKED;
}
private String toDebugString(UnfilteredRowIterator partition, TableMetadata metadata)
{
StringBuilder sb = new StringBuilder();

View File

@ -97,10 +97,12 @@ import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.NoSpamLogger;
import org.apache.cassandra.utils.btree.BTreeSet;
import static org.apache.cassandra.db.ReadKind.UNTRACKED;
/**
* A read command that selects a (part of a) single partition.
*/
public class SinglePartitionReadCommand extends ReadCommand implements SinglePartitionReadQuery
public class SinglePartitionReadCommand extends ReadCommand implements SinglePartitionReadQuery, EmbeddableSinglePartitionReadCommand
{
private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1L, TimeUnit.SECONDS);
protected static final SelectionDeserializer selectionDeserializer = new Deserializer();
@ -125,7 +127,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar
boolean trackWarnings,
DataRange dataRange)
{
super(serializedAtEpoch, Kind.SINGLE_PARTITION, isDigest, digestVersion, potentialTxnConflicts, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings, dataRange);
super(serializedAtEpoch, ReadCommand.Kind.SINGLE_PARTITION, isDigest, digestVersion, potentialTxnConflicts, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings, dataRange);
assert partitionKey.getPartitioner() == metadata.partitioner;
this.partitionKey = partitionKey;
this.clusteringIndexFilter = clusteringIndexFilter;
@ -1375,6 +1377,12 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar
return false;
}
@Override
public ReadKind kind()
{
return UNTRACKED;
}
/*
* When running transactionally we need to use the txn system nowInSeconds, and set whether reconciliation
* should be performed based on whether it's part of a multiple replica read. We also allow potential txn conflicts

View File

@ -261,6 +261,7 @@ public final class SystemKeyspace
+ "in_progress_read_ballot timeuuid,"
+ "most_recent_commit blob,"
+ "most_recent_commit_at timeuuid,"
+ "most_recent_commit_mutation_id blob,"
+ "most_recent_commit_version int,"
+ "proposal blob,"
+ "proposal_ballot timeuuid,"
@ -1584,7 +1585,7 @@ public final class SystemKeyspace
if (proposal instanceof AcceptedWithTTL)
{
long localDeletionTime = ((Commit.AcceptedWithTTL) proposal).localDeletionTime;
int ttlInSec = legacyPaxosTtlSec(proposal.update.metadata());
int ttlInSec = legacyPaxosTtlSec(proposal.metadata());
long nowInSec = localDeletionTime - ttlInSec;
String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? AND TTL ? SET proposal_ballot = ?, proposal = ?, proposal_version = ? WHERE row_key = ? AND cf_id = ?";
executeInternalWithNowInSec(cql,
@ -1594,8 +1595,8 @@ public final class SystemKeyspace
proposal.ballot,
PartitionUpdate.toBytes(proposal.update, MessagingService.current_version),
MessagingService.current_version,
proposal.update.partitionKey().getKey(),
proposal.update.metadata().id.asUUID());
proposal.partitionKey().getKey(),
proposal.metadata().id.asUUID());
}
else
{
@ -1605,8 +1606,8 @@ public final class SystemKeyspace
proposal.ballot,
PartitionUpdate.toBytes(proposal.update, MessagingService.current_version),
MessagingService.current_version,
proposal.update.partitionKey().getKey(),
proposal.update.metadata().id.asUUID());
proposal.partitionKey().getKey(),
proposal.metadata().id.asUUID());
}
}
@ -1614,12 +1615,14 @@ public final class SystemKeyspace
{
// We always erase the last proposal (with the commit timestamp to no erase more recent proposal in case the commit is old)
// even though that's really just an optimization since SP.beginAndRepairPaxos will exclude accepted proposal older than the mrc.
ByteBuffer mutationIdBytes = commit.mutation.id().isNone() ? null : commit.mutation.id().toByteBuffer();
if (commit instanceof Commit.CommittedWithTTL)
{
long localDeletionTime = ((Commit.CommittedWithTTL) commit).localDeletionTime;
int ttlInSec = legacyPaxosTtlSec(commit.update.metadata());
int ttlInSec = legacyPaxosTtlSec(commit.metadata());
long nowInSec = localDeletionTime - ttlInSec;
String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? AND TTL ? SET proposal_ballot = null, proposal = null, proposal_version = null, most_recent_commit_at = ?, most_recent_commit = ?, most_recent_commit_version = ? WHERE row_key = ? AND cf_id = ?";
String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? AND TTL ? SET proposal_ballot = null, proposal = null, proposal_version = null, most_recent_commit_at = ?, most_recent_commit = ?, most_recent_commit_version = ?, most_recent_commit_mutation_id = ? WHERE row_key = ? AND cf_id = ?";
executeInternalWithNowInSec(cql,
nowInSec,
commit.ballot.unixMicros(),
@ -1627,19 +1630,21 @@ public final class SystemKeyspace
commit.ballot,
PartitionUpdate.toBytes(commit.update, MessagingService.current_version),
MessagingService.current_version,
commit.update.partitionKey().getKey(),
commit.update.metadata().id.asUUID());
mutationIdBytes,
commit.partitionKey().getKey(),
commit.metadata().id.asUUID());
}
else
{
String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? SET proposal_ballot = null, proposal = null, proposal_version = null, most_recent_commit_at = ?, most_recent_commit = ?, most_recent_commit_version = ? WHERE row_key = ? AND cf_id = ?";
String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? SET proposal_ballot = null, proposal = null, proposal_version = null, most_recent_commit_at = ?, most_recent_commit = ?, most_recent_commit_version = ?, most_recent_commit_mutation_id = ? WHERE row_key = ? AND cf_id = ?";
executeInternal(cql,
commit.ballot.unixMicros(),
commit.ballot,
PartitionUpdate.toBytes(commit.update, MessagingService.current_version),
MessagingService.current_version,
commit.update.partitionKey().getKey(),
commit.update.metadata().id.asUUID());
mutationIdBytes,
commit.partitionKey().getKey(),
commit.metadata().id.asUUID());
}
}

View File

@ -17,19 +17,76 @@
*/
package org.apache.cassandra.db;
import java.io.IOException;
import org.apache.cassandra.io.UnversionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
/**
* Identifier for what type of operation timed out. This type is driver facing as a String, but some drivers convert
* this to an enum, meaning any changes to this type require protocol changes and driver support.
*/
public enum WriteType
{
SIMPLE,
BATCH,
UNLOGGED_BATCH,
COUNTER,
BATCH_LOG,
CAS,
VIEW,
CDC
SIMPLE (0),
BATCH (1),
UNLOGGED_BATCH (2),
COUNTER (3),
BATCH_LOG (4),
CAS (5),
VIEW (6),
CDC (7);
//TODO update client protocol to support "TRANSACTION"
// used by the messaging service
public final int id;
private static final WriteType[] idToTypeMapping;
static
{
int maxId = -1;
for (WriteType type : WriteType.values())
maxId = Math.max(maxId, type.id);
idToTypeMapping = new WriteType[maxId + 1];
for (WriteType type : WriteType.values())
{
if (idToTypeMapping[type.id] != null)
throw new IllegalStateException("Duplicate id " + type.id);
idToTypeMapping[type.id] = type;
}
}
WriteType(int id)
{
this.id = id;
}
public static WriteType fromId(int id) throws IOException
{
if (id < 0 || id >= idToTypeMapping.length)
throw new IllegalArgumentException("Unknown WriteType id: " + id);
return idToTypeMapping[id];
}
public static final UnversionedSerializer<WriteType> serializer = new UnversionedSerializer<>()
{
@Override
public void serialize(WriteType writeType, DataOutputPlus out) throws IOException
{
out.writeByte(writeType.id);
}
@Override
public WriteType deserialize(DataInputPlus in) throws IOException
{
return fromId(in.readUnsignedByte());
}
@Override
public long serializedSize(WriteType writeType)
{
return TypeSizes.BYTE_SIZE;
}
};
}

View File

@ -18,12 +18,24 @@
package org.apache.cassandra.db.guardrails;
import org.apache.cassandra.exceptions.CassandraExceptionCode;
import org.apache.cassandra.exceptions.InvalidRequestException;
public class GuardrailViolatedException extends InvalidRequestException
{
GuardrailViolatedException(String message)
public GuardrailViolatedException(String message)
{
super(message);
}
protected GuardrailViolatedException(String message, Throwable cause)
{
super(message, cause);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.GUARDRAIL_VIOLATED;
}
}

View File

@ -81,6 +81,7 @@ import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.accord.serializers.TableMetadatas;
import org.apache.cassandra.service.paxos.Commit.Commitable;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.utils.Pair;
@ -106,7 +107,7 @@ import static org.apache.cassandra.db.rows.UnfilteredRowIteratorSerializer.IS_EM
* is also a few static helper constructor methods for special cases ({@code emptyUpdate()},
* {@code fullPartitionDelete} and {@code singleRowUpdate}).
*/
public class PartitionUpdate extends AbstractBTreePartition
public class PartitionUpdate extends AbstractBTreePartition implements Commitable
{
protected static final Logger logger = LoggerFactory.getLogger(PartitionUpdate.class);
@ -618,6 +619,12 @@ public class PartitionUpdate extends AbstractBTreePartition
return super.equals(obj);
}
@Override
public CommitableKind commitableKind()
{
return CommitableKind.PARTITION_UPDATE;
}
/**
* Interface for building partition updates geared towards human.
* <p>

View File

@ -17,6 +17,12 @@
*/
package org.apache.cassandra.exceptions;
import java.io.IOException;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
public class AlreadyExistsException extends ConfigurationException
{
public final String ksName;
@ -39,4 +45,29 @@ public class AlreadyExistsException extends ConfigurationException
this(ksName, "", String.format("Cannot add existing keyspace \"%s\"", ksName));
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
out.writeUTF(ksName);
out.writeUTF(cfName);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return TypeSizes.sizeof(ksName) + TypeSizes.sizeof(cfName);
}
static AlreadyExistsException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
String ksName = in.readUTF();
String cfName = in.readUTF();
return new AlreadyExistsException(ksName, cfName, message);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.ALREADY_EXISTS;
}
}

View File

@ -28,4 +28,10 @@ public class AuthenticationException extends RequestValidationException
{
super(ExceptionCode.BAD_CREDENTIALS, msg, e);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.BAD_CREDENTIALS;
}
}

View File

@ -23,4 +23,10 @@ public class CDCWriteException extends RequestExecutionException
{
super(ExceptionCode.CDC_WRITE_FAILURE, msg);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.CDC_WRITE_FAILURE;
}
}

View File

@ -17,9 +17,13 @@
*/
package org.apache.cassandra.exceptions;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.WriteType;
import java.io.IOException;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
public class CasWriteTimeoutException extends WriteTimeoutException
{
@ -30,4 +34,33 @@ public class CasWriteTimeoutException extends WriteTimeoutException
super(writeType, consistency, received, blockFor, String.format("CAS operation timed out: received %d of %d required responses after %d contention retries", received, blockFor, contentions));
this.contentions = contentions;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
super.serializeSpecificFields(out, version);
out.writeUnsignedVInt32(contentions);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return super.serializedSizeSpecificFields(version) + TypeSizes.sizeofUnsignedVInt(contentions);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.CAS_WRITE_TIMEOUT;
}
static CasWriteTimeoutException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte());
int received = in.readUnsignedVInt32();
int blockFor = in.readUnsignedVInt32();
WriteType writeType = WriteType.serializer.deserialize(in);
int contentions = in.readUnsignedVInt32();
return new CasWriteTimeoutException(writeType, consistency, received, blockFor, contentions);
}
}

View File

@ -18,7 +18,12 @@
package org.apache.cassandra.exceptions;
import java.io.IOException;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
public class CasWriteUnknownResultException extends RequestExecutionException
{
@ -33,4 +38,34 @@ public class CasWriteUnknownResultException extends RequestExecutionException
this.received = received;
this.blockFor = blockFor;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
out.writeByte(consistency.code);
out.writeUnsignedVInt32(received);
out.writeUnsignedVInt32(blockFor);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return TypeSizes.BYTE_SIZE + // consistency
TypeSizes.sizeofUnsignedVInt(received) +
TypeSizes.sizeofUnsignedVInt(blockFor);
}
static CasWriteUnknownResultException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte());
int received = in.readUnsignedVInt32();
int blockFor = in.readUnsignedVInt32();
return new CasWriteUnknownResultException(consistency, received, blockFor);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.CAS_WRITE_UNKNOWN;
}
}

View File

@ -17,6 +17,23 @@
*/
package org.apache.cassandra.exceptions;
import java.io.IOException;
import org.apache.cassandra.cql3.constraints.ConstraintViolationException;
import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException;
import org.apache.cassandra.db.KeyspaceNotDefinedException;
import org.apache.cassandra.db.MutationExceededMaxSizeException;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.guardrails.GuardrailViolatedException;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.service.accord.exceptions.AccordReadExhaustedException;
import org.apache.cassandra.service.accord.exceptions.AccordReadPreemptedException;
import org.apache.cassandra.service.accord.exceptions.AccordWriteExhaustedException;
import org.apache.cassandra.service.accord.exceptions.AccordWritePreemptedException;
import org.apache.cassandra.triggers.TriggerDisabledException;
import org.apache.cassandra.utils.ArraySerializers;
import org.apache.cassandra.utils.Shared;
import static org.apache.cassandra.utils.Shared.Scope.SIMULATION;
@ -24,22 +41,180 @@ import static org.apache.cassandra.utils.Shared.Scope.SIMULATION;
@Shared(scope = SIMULATION)
public abstract class CassandraException extends RuntimeException implements TransportException
{
private final ExceptionCode code;
public static final Serializer serializer = new Serializer();
private final ExceptionCode clientExceptionCode;
protected CassandraException(ExceptionCode code, String msg)
{
super(msg);
this.code = code;
this.clientExceptionCode = code;
}
protected CassandraException(ExceptionCode code, String msg, Throwable cause)
{
super(msg, cause);
this.code = code;
this.clientExceptionCode = code;
}
public ExceptionCode code()
{
return code;
return clientExceptionCode;
}
/**
* Returns the CassandraExceptionCode for this specific exception class.
* Each subclass must implement this to return its unique serialization code.
*/
public abstract CassandraExceptionCode getCassandraExceptionCode();
/**
* Serializer for CassandraException that coordinates serialization of subclasses.
*/
@Shared(scope = SIMULATION)
public static class Serializer implements IVersionedSerializer<CassandraException>
{
@Override
public void serialize(CassandraException exception, DataOutputPlus out, int version) throws IOException
{
String message = exception.getMessage() == null ? "" : exception.getMessage();
out.writeUnsignedVInt32(exception.getCassandraExceptionCode().value);
out.writeUTF(message);
ExceptionSerializer.nullableRemoteExceptionSerializer.serialize(exception.getCause(), out, version);
ArraySerializers.serializeArray(exception.getSuppressed(), out, version, ExceptionSerializer.remoteExceptionSerializer);
StackTraceElement[] stackTrace = exception.getStackTrace();
ArraySerializers.serializeArray(stackTrace, out, version, ExceptionSerializer.stackTraceElementSerializer);
// delegate to subclass serializer for type-specific fields
exception.serializeSpecificFields(out, version);
}
@Override
public CassandraException deserialize(DataInputPlus in, int version) throws IOException
{
CassandraExceptionCode classCode = CassandraExceptionCode.fromValue(in.readUnsignedVInt32());
String message = in.readUTF();
Throwable cause = ExceptionSerializer.nullableRemoteExceptionSerializer.deserialize(in, version);
Throwable[] suppressed = ArraySerializers.deserializeArray(in, version, ExceptionSerializer.remoteExceptionSerializer, Throwable[]::new);
StackTraceElement[] stackTrace = ArraySerializers.deserializeArray(in, version, ExceptionSerializer.stackTraceElementSerializer, StackTraceElement[]::new);
// delegate to subclass serializer for type-specific fields
CassandraException exception = deserializeByClassCode(classCode, message, in, version);
if (cause != null)
exception.initCause(cause);
for (Throwable t : suppressed)
exception.addSuppressed(t);
exception.setStackTrace(stackTrace);
return exception;
}
@Override
public long serializedSize(CassandraException exception, int version)
{
String message = exception.getMessage() == null ? "" : exception.getMessage();
long size = TypeSizes.sizeofUnsignedVInt(exception.getCassandraExceptionCode().value);
size += TypeSizes.sizeof(message);
size += ExceptionSerializer.nullableRemoteExceptionSerializer.serializedSize(exception.getCause(), version);
size += ArraySerializers.serializedArraySize(exception.getSuppressed(), version, ExceptionSerializer.remoteExceptionSerializer);
size += ArraySerializers.serializedArraySize(exception.getStackTrace(), version, ExceptionSerializer.stackTraceElementSerializer);
// delegate to subclass serializer for type-specific fields
size += exception.serializedSizeSpecificFields(version);
return size;
}
private CassandraException deserializeByClassCode(CassandraExceptionCode cassandraExceptionCode, String message, DataInputPlus in, int version) throws IOException
{
// Create the appropriate exception instance based on the class code
switch (cassandraExceptionCode)
{
case UNAVAILABLE:
return UnavailableException.deserializeFields(message, in, version);
case FUNCTION_FAILURE:
return FunctionExecutionException.deserializeFields(message, in, version);
case OPERATION_EXECUTION:
return OperationExecutionException.deserializeFields(message, in, version);
case OVERLOADED:
return new OverloadedException(message);
case CAS_WRITE_UNKNOWN:
return CasWriteUnknownResultException.deserializeFields(message, in, version);
case IS_BOOTSTRAPPING:
return new IsBootstrappingException();
case CDC_WRITE_FAILURE:
return new CDCWriteException(message);
case TRUNCATE_ERROR:
return new TruncateException(message);
case READ_TIMEOUT:
return ReadTimeoutException.deserializeFields(message, in, version);
case ACCORD_READ_EXHAUSTED:
return AccordReadExhaustedException.deserializeFields(message, in, version);
case ACCORD_READ_PREEMPTED:
return AccordReadPreemptedException.deserializeFields(message, in, version);
case ACCORD_WRITE_PREEMPTED:
return AccordWritePreemptedException.deserializeFields(message, in, version);
case ACCORD_WRITE_EXHAUSTED:
return AccordWriteExhaustedException.deserializeFields(message, in, version);
case WRITE_TIMEOUT:
return WriteTimeoutException.deserializeFields(message, in, version);
case CAS_WRITE_TIMEOUT:
return CasWriteTimeoutException.deserializeFields(message, in, version);
case READ_FAILURE:
return ReadFailureException.deserializeFields(message, in, version);
case TOMBSTONE_ABORT:
return TombstoneAbortException.deserializeFields(message, in, version);
case READ_SIZE_ABORT:
return ReadSizeAbortException.deserializeFields(message, in, version);
case QUERY_TOO_MANY_INDEXES_ABORT:
return QueryReferencesTooManyIndexesAbortException.deserializeFields(message, in, version);
case WRITE_FAILURE:
return WriteFailureException.deserializeFields(message, in, version);
case UNPREPARED:
return PreparedQueryNotFoundException.deserializeFields(message, in, version);
case BAD_CREDENTIALS:
return new AuthenticationException(message);
case CONFIG_ERROR:
return new ConfigurationException(message);
case ALREADY_EXISTS:
return AlreadyExistsException.deserializeFields(message, in, version);
case UNAUTHORIZED:
return new UnauthorizedException(message);
case INVALID:
return new InvalidRequestException(message);
case INVALID_CONSTRAINT_DEFINITION:
return new InvalidConstraintDefinitionException(message);
case CONSTRAINT_VIOLATION:
return new ConstraintViolationException(message);
case TRIGGER_DISABLED:
return new TriggerDisabledException(message);
case KEYSPACE_NOT_DEFINED:
return new KeyspaceNotDefinedException(message);
case GUARDRAIL_VIOLATED:
return new GuardrailViolatedException(message);
case MUTATION_EXCEEDED_MAX_SIZE:
return MutationExceededMaxSizeException.deserializeFields(message, in, version);
case OVERSIZED_MESSAGE:
return new OversizedCQLMessageException(message);
case INVALID_ROUTING:
return new InvalidRoutingException(message);
case SYNTAX_ERROR:
return new SyntaxException(message);
default:
throw new AssertionError("Unhandled CassandraExceptionCode: " + cassandraExceptionCode);
}
}
}
/**
* Serialize subclass-specific fields. Override in subclasses that have additional fields.
*/
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
}
/**
* Calculate serialized size of subclass-specific fields. Override in subclasses that have additional fields.
*/
protected long serializedSizeSpecificFields(int version)
{
return 0;
}
}

View File

@ -0,0 +1,96 @@
/*
* 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.exceptions;
import java.util.HashMap;
import java.util.Map;
import org.apache.cassandra.utils.Shared;
import static org.apache.cassandra.utils.Shared.Scope.SIMULATION;
/**
* Exception codes for internal CassandraException serialization.
* Each concrete CassandraException subclass has a unique code to ensure proper type fidelity during serialization.
* This is separate from the client-facing ExceptionCode enum which maintains backward compatibility.
*/
@Shared(scope = SIMULATION)
public enum CassandraExceptionCode
{
// Execution exceptions
UNAVAILABLE (0), // UnavailableException
FUNCTION_FAILURE (1), // FunctionExecutionException
OPERATION_EXECUTION (2), // OperationExecutionException
OVERLOADED (3), // OverloadedException
CAS_WRITE_UNKNOWN (4), // CasWriteUnknownResultException
IS_BOOTSTRAPPING (5), // IsBootstrappingException
CDC_WRITE_FAILURE (6), // CDCWriteException
TRUNCATE_ERROR (7), // TruncateException
// Timeout exceptions
READ_TIMEOUT (8), // ReadTimeoutException
ACCORD_READ_EXHAUSTED (9), // AccordReadExhaustedException
ACCORD_READ_PREEMPTED (10), // AccordReadPreemptedException
WRITE_TIMEOUT (11), // WriteTimeoutException
ACCORD_WRITE_PREEMPTED (12), // AccordWritePreemptedException
ACCORD_WRITE_EXHAUSTED (13), // AccordWriteExhaustedException
CAS_WRITE_TIMEOUT (14), // CasWriteTimeoutException
// Failure exceptions
READ_FAILURE (15), // ReadFailureException
TOMBSTONE_ABORT (16), // TombstoneAbortException
READ_SIZE_ABORT (17), // ReadSizeAbortException
QUERY_TOO_MANY_INDEXES_ABORT (18), // QueryReferencesTooManyIndexesAbortException
WRITE_FAILURE (19), // WriteFailureException
// Validation/config exceptions
UNPREPARED (20), // PreparedQueryNotFoundException
BAD_CREDENTIALS (21), // AuthenticationException
CONFIG_ERROR (22), // ConfigurationException
ALREADY_EXISTS (23), // AlreadyExistsException
UNAUTHORIZED (24), // UnauthorizedException
INVALID (25), // InvalidRequestException
INVALID_CONSTRAINT_DEFINITION (26), // InvalidConstraintDefinitionException
CONSTRAINT_VIOLATION (27), // ConstraintViolationException
OVERSIZED_MESSAGE (28), // OversizedCQLMessageException
TRIGGER_DISABLED (29), // TriggerDisabledException
KEYSPACE_NOT_DEFINED (30), // KeyspaceNotDefinedException
GUARDRAIL_VIOLATED (31), // GuardrailViolatedException
INVALID_ROUTING (32), // InvalidRoutingException
MUTATION_EXCEEDED_MAX_SIZE (33), // MutationExceededMaxSizeException
SYNTAX_ERROR (34); // SyntaxException
public final int value;
private static final Map<Integer, CassandraExceptionCode> valueToCode = new HashMap<>(CassandraExceptionCode.values().length);
static
{
for (CassandraExceptionCode code : CassandraExceptionCode.values())
valueToCode.put(code.value, code);
}
CassandraExceptionCode(int value)
{
this.value = value;
}
public static CassandraExceptionCode fromValue(int value)
{
CassandraExceptionCode code = valueToCode.get(value);
if (code == null)
throw new IllegalArgumentException(String.format("Unknown CassandraException code %d", value));
return code;
}
}

View File

@ -49,4 +49,10 @@ public class ConfigurationException extends RequestValidationException
super(code, msg);
logStackTrace = true;
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.CONFIG_ERROR;
}
}

View File

@ -78,7 +78,7 @@ public class ExceptionSerializer
return t.getLocalizedMessage();
}
private static final IVersionedSerializer<StackTraceElement> stackTraceElementSerializer = new IVersionedSerializer<StackTraceElement>()
static final IVersionedSerializer<StackTraceElement> stackTraceElementSerializer = new IVersionedSerializer<StackTraceElement>()
{
@Override
public void serialize(StackTraceElement t, DataOutputPlus out, int version) throws IOException

View File

@ -17,11 +17,16 @@
*/
package org.apache.cassandra.exceptions;
import java.io.IOException;
import java.util.List;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.functions.FunctionName;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.utils.CollectionSerializers;
public class FunctionExecutionException extends RequestExecutionException
{
@ -50,4 +55,42 @@ public class FunctionExecutionException extends RequestExecutionException
this.argTypes = argTypes;
this.detail = msg;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
out.writeBoolean(functionName.keyspace != null);
if (functionName.keyspace != null)
out.writeUTF(functionName.keyspace);
out.writeUTF(functionName.name);
CollectionSerializers.serializeList(argTypes, out, version, CollectionSerializers.STRING_SERIALIZER);
out.writeUTF(detail);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
long size = TypeSizes.BOOL_SIZE; // keyspace present flag
if (functionName.keyspace != null)
size += TypeSizes.sizeof(functionName.keyspace);
size += TypeSizes.sizeof(functionName.name);
size += CollectionSerializers.serializedListSize(argTypes, version, CollectionSerializers.STRING_SERIALIZER);
size += TypeSizes.sizeof(detail);
return size;
}
static FunctionExecutionException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
String keyspace = in.readBoolean() ? in.readUTF() : null;
String name = in.readUTF();
List<String> argTypes = CollectionSerializers.deserializeList(in, version, CollectionSerializers.STRING_SERIALIZER);
String detail = in.readUTF();
return new FunctionExecutionException(new FunctionName(keyspace, name), argTypes, detail);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.FUNCTION_FAILURE;
}
}

View File

@ -28,4 +28,10 @@ public class InvalidRequestException extends RequestValidationException
{
super(ExceptionCode.INVALID, msg, t);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.INVALID;
}
}

View File

@ -31,7 +31,7 @@ public class InvalidRoutingException extends InvalidRequestException
public static final String RANGE_TEMPLATE = "Received a read request from %s for a range [%s,%s] that is not owned by the current replica as of %s: %s.";
public static final String WRITE_TEMPLATE = "Received a mutation from %s for a token %s that is not owned by the current replica as of %s: %s.";
private InvalidRoutingException(String msg)
public InvalidRoutingException(String msg)
{
super(msg);
}
@ -59,4 +59,10 @@ public class InvalidRoutingException extends InvalidRequestException
{
return new InvalidRoutingException(String.format(WRITE_TEMPLATE, from, token, epoch, mutation.getKeyspaceName()));
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.INVALID_ROUTING;
}
}

View File

@ -23,4 +23,10 @@ public class IsBootstrappingException extends RequestExecutionException
{
super(ExceptionCode.IS_BOOTSTRAPPING, "Cannot read from a bootstrapping node");
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.IS_BOOTSTRAPPING;
}
}

View File

@ -17,10 +17,12 @@
*/
package org.apache.cassandra.exceptions;
import java.io.IOException;
import java.util.List;
import org.apache.cassandra.cql3.functions.OperationFcts;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.io.util.DataInputPlus;
/**
* Thrown when an operation problem has occured (e.g. division by zero with integer).
@ -54,4 +56,22 @@ public final class OperationExecutionException extends FunctionExecutionExceptio
super(OperationFcts.getFunctionNameFromOperator(operator), argTypes, msg);
}
static OperationExecutionException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
// Deserialize using parent's logic to get FunctionName, argTypes, and detail
FunctionExecutionException parent = FunctionExecutionException.deserializeFields(message, in, version);
// Extract operator from function name using OperationFcts
char operator = OperationFcts.getOperator(parent.functionName);
// Create OperationExecutionException with the extracted operator
return new OperationExecutionException(operator, parent.argTypes, parent.detail);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.OPERATION_EXECUTION;
}
}

View File

@ -23,4 +23,10 @@ public class OverloadedException extends RequestExecutionException
{
super(ExceptionCode.OVERLOADED, reason);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.OVERLOADED;
}
}

View File

@ -24,4 +24,10 @@ public class OversizedCQLMessageException extends InvalidRequestException
{
super(message);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.OVERSIZED_MESSAGE;
}
}

View File

@ -17,6 +17,10 @@
*/
package org.apache.cassandra.exceptions;
import java.io.IOException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.utils.MD5Digest;
public class PreparedQueryNotFoundException extends RequestValidationException
@ -36,4 +40,31 @@ public class PreparedQueryNotFoundException extends RequestValidationException
" or you have prepared too many queries and it has been evicted from the internal cache)",
id);
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
// MD5 digest is always 16 bytes
out.write(id.bytes);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return id.bytes.length; // MD5 is always 16 bytes
}
static PreparedQueryNotFoundException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
byte[] bytes = new byte[16]; // MD5 is always 16 bytes
in.readFully(bytes);
MD5Digest id = MD5Digest.wrap(bytes);
return new PreparedQueryNotFoundException(id);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.UNPREPARED;
}
}

View File

@ -18,9 +18,13 @@
package org.apache.cassandra.exceptions;
import java.io.IOException;
import java.util.Map;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
public class QueryReferencesTooManyIndexesAbortException extends ReadAbortException
@ -34,4 +38,38 @@ public class QueryReferencesTooManyIndexesAbortException extends ReadAbortExcept
this.nodes = nodes;
this.maxValue = maxValue;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
// Serialize parent fields first
super.serializeSpecificFields(out, version);
// Add QueryReferencesTooManyIndexesAbortException specific fields
out.writeUnsignedVInt32(nodes);
out.writeUnsignedVInt(maxValue);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return super.serializedSizeSpecificFields(version) +
TypeSizes.sizeofUnsignedVInt(nodes) +
TypeSizes.sizeofUnsignedVInt(maxValue);
}
static QueryReferencesTooManyIndexesAbortException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
ReadFailureException.DeserializedFields fields = ReadFailureException.deserializeBaseFields(in, version);
int nodes = in.readUnsignedVInt32();
long maxValue = in.readUnsignedVInt();
return new QueryReferencesTooManyIndexesAbortException(message, nodes, maxValue, fields.dataPresent,
fields.consistency, fields.received, fields.blockFor, fields.failures);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.QUERY_TOO_MANY_INDEXES_ABORT;
}
}

View File

@ -17,12 +17,17 @@
*/
package org.apache.cassandra.exceptions;
import java.io.IOException;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.utils.CollectionSerializers;
public class ReadFailureException extends RequestFailureException
{
@ -51,4 +56,88 @@ public class ReadFailureException extends RequestFailureException
super(ExceptionCode.READ_FAILURE, rfe.getMessage(), rfe.consistency, rfe.received, rfe.blockFor, rfe.failureReasonByEndpoint, rfe);
this.dataPresent = rfe.dataPresent;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
out.writeByte(consistency.code);
out.writeUnsignedVInt32(received);
out.writeUnsignedVInt32(blockFor);
// Serialize failure reason map
CollectionSerializers.serializeMap(failureReasonByEndpoint, out, version,
InetAddressAndPort.Serializer.inetAddressAndPortSerializer,
RequestFailureReason.serializer);
out.writeBoolean(dataPresent);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
long size = TypeSizes.BYTE_SIZE + // consistency
TypeSizes.sizeofUnsignedVInt(received) +
TypeSizes.sizeofUnsignedVInt(blockFor);
size += CollectionSerializers.serializedMapSize(failureReasonByEndpoint, version,
InetAddressAndPort.Serializer.inetAddressAndPortSerializer,
RequestFailureReason.serializer);
size += TypeSizes.BOOL_SIZE; // dataPresent
return size;
}
static ReadFailureException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
DeserializedFields fields = deserializeBaseFields(in, version);
return new ReadFailureException(message, fields.consistency, fields.received, fields.blockFor, fields.dataPresent, fields.failures);
}
/**
* Helper class to hold deserialized base fields for subclasses to use.
*/
static class DeserializedFields
{
final ConsistencyLevel consistency;
final int received;
final int blockFor;
final Map<InetAddressAndPort, RequestFailureReason> failures;
final boolean dataPresent;
DeserializedFields(ConsistencyLevel consistency, int received, int blockFor,
Map<InetAddressAndPort, RequestFailureReason> failures, boolean dataPresent)
{
this.consistency = consistency;
this.received = received;
this.blockFor = blockFor;
this.failures = failures;
this.dataPresent = dataPresent;
}
}
/**
* Deserialize the base fields common to ReadFailureException and its subclasses.
* Subclasses should call this method and then read any additional fields.
*/
static DeserializedFields deserializeBaseFields(DataInputPlus in, int version) throws IOException
{
ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte());
int received = in.readUnsignedVInt32();
int blockFor = in.readUnsignedVInt32();
// Deserialize failure reason map
Map<InetAddressAndPort, RequestFailureReason> failures =
CollectionSerializers.deserializeMap(in, version,
InetAddressAndPort.Serializer.inetAddressAndPortSerializer,
RequestFailureReason.serializer);
boolean dataPresent = in.readBoolean();
return new DeserializedFields(consistency, received, blockFor, failures, dataPresent);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.READ_FAILURE;
}
}

View File

@ -18,9 +18,11 @@
package org.apache.cassandra.exceptions;
import java.io.IOException;
import java.util.Map;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
public class ReadSizeAbortException extends ReadAbortException
@ -29,4 +31,16 @@ public class ReadSizeAbortException extends ReadAbortException
{
super(msg, consistency, received, blockFor, dataPresent, failureReasonByEndpoint);
}
static ReadSizeAbortException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
ReadFailureException.DeserializedFields fields = ReadFailureException.deserializeBaseFields(in, version);
return new ReadSizeAbortException(message, fields.consistency, fields.received, fields.blockFor, fields.dataPresent, fields.failures);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.READ_SIZE_ABORT;
}
}

View File

@ -17,7 +17,12 @@
*/
package org.apache.cassandra.exceptions;
import java.io.IOException;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
public class ReadTimeoutException extends RequestTimeoutException
{
@ -46,4 +51,37 @@ public class ReadTimeoutException extends RequestTimeoutException
super(ExceptionCode.READ_TIMEOUT, rfe.consistency, rfe.received, rfe.blockFor, rfe);
this.dataPresent = rfe.dataPresent;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
out.writeByte(consistency.code);
out.writeUnsignedVInt32(received);
out.writeUnsignedVInt32(blockFor);
out.writeBoolean(dataPresent);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return TypeSizes.BYTE_SIZE + // consistency
TypeSizes.sizeofUnsignedVInt(received) +
TypeSizes.sizeofUnsignedVInt(blockFor) +
TypeSizes.BOOL_SIZE; // dataPresent
}
static ReadTimeoutException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte());
int received = in.readUnsignedVInt32();
int blockFor = in.readUnsignedVInt32();
boolean dataPresent = in.readBoolean();
return new ReadTimeoutException(consistency, received, blockFor, dataPresent, message);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.READ_TIMEOUT;
}
}

View File

@ -23,7 +23,7 @@ import java.util.stream.Collectors;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.locator.InetAddressAndPort;
public class RequestFailureException extends RequestExecutionException
public abstract class RequestFailureException extends RequestExecutionException
{
public final ConsistencyLevel consistency;
public final int received;

View File

@ -19,7 +19,7 @@ package org.apache.cassandra.exceptions;
import org.apache.cassandra.db.ConsistencyLevel;
public class RequestTimeoutException extends RequestExecutionException
public abstract class RequestTimeoutException extends RequestExecutionException
{
public final ConsistencyLevel consistency;
public final int received;

View File

@ -23,4 +23,10 @@ public class SyntaxException extends RequestValidationException
{
super(ExceptionCode.SYNTAX_ERROR, msg);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.SYNTAX_ERROR;
}
}

View File

@ -18,9 +18,13 @@
package org.apache.cassandra.exceptions;
import java.io.IOException;
import java.util.Map;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
public class TombstoneAbortException extends ReadAbortException
@ -36,4 +40,38 @@ public class TombstoneAbortException extends ReadAbortException
this.nodes = nodes;
this.tombstones = tombstones;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
// Serialize parent fields first
super.serializeSpecificFields(out, version);
// Add TombstoneAbortException specific fields
out.writeUnsignedVInt32(nodes);
out.writeUnsignedVInt(tombstones);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return super.serializedSizeSpecificFields(version) +
TypeSizes.sizeofUnsignedVInt(nodes) +
TypeSizes.sizeofUnsignedVInt(tombstones);
}
static TombstoneAbortException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
ReadFailureException.DeserializedFields fields = ReadFailureException.deserializeBaseFields(in, version);
int nodes = in.readUnsignedVInt32();
long tombstones = in.readUnsignedVInt();
return new TombstoneAbortException(message, nodes, tombstones, fields.dataPresent,
fields.consistency, fields.received, fields.blockFor, fields.failures);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.TOMBSTONE_ABORT;
}
}

View File

@ -28,4 +28,10 @@ public class TruncateException extends RequestExecutionException
{
super(ExceptionCode.TRUNCATE_ERROR, msg);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.TRUNCATE_ERROR;
}
}

View File

@ -28,4 +28,10 @@ public class UnauthorizedException extends RequestValidationException
{
super(ExceptionCode.UNAUTHORIZED, msg, e);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.UNAUTHORIZED;
}
}

View File

@ -17,7 +17,12 @@
*/
package org.apache.cassandra.exceptions;
import java.io.IOException;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
public class UnavailableException extends RequestExecutionException
{
@ -55,4 +60,34 @@ public class UnavailableException extends RequestExecutionException
this.required = required;
this.alive = alive;
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.UNAVAILABLE;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
out.writeByte(consistency.code);
out.writeUnsignedVInt32(required);
out.writeUnsignedVInt32(alive);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return TypeSizes.BYTE_SIZE + // consistency
TypeSizes.sizeofUnsignedVInt(required) +
TypeSizes.sizeofUnsignedVInt(alive);
}
static UnavailableException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte());
int required = in.readUnsignedVInt32();
int alive = in.readUnsignedVInt32();
return create(consistency, required, alive);
}
}

View File

@ -17,13 +17,18 @@
*/
package org.apache.cassandra.exceptions;
import java.io.IOException;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.utils.CollectionSerializers;
public class WriteFailureException extends RequestFailureException
{
@ -34,4 +39,56 @@ public class WriteFailureException extends RequestFailureException
super(ExceptionCode.WRITE_FAILURE, consistency, received, blockFor, ImmutableMap.copyOf(failureReasonByEndpoint));
this.writeType = writeType;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
out.writeByte(consistency.code);
out.writeUnsignedVInt32(received);
out.writeUnsignedVInt32(blockFor);
// Serialize failure reason map
CollectionSerializers.serializeMap(failureReasonByEndpoint, out, version,
InetAddressAndPort.Serializer.inetAddressAndPortSerializer,
RequestFailureReason.serializer);
WriteType.serializer.serialize(writeType, out);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
long size = TypeSizes.BYTE_SIZE + // consistency
TypeSizes.sizeofUnsignedVInt(received) +
TypeSizes.sizeofUnsignedVInt(blockFor);
size += CollectionSerializers.serializedMapSize(failureReasonByEndpoint, version,
InetAddressAndPort.Serializer.inetAddressAndPortSerializer,
RequestFailureReason.serializer);
size += WriteType.serializer.serializedSize(writeType);
return size;
}
static WriteFailureException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte());
int received = in.readUnsignedVInt32();
int blockFor = in.readUnsignedVInt32();
// Deserialize failure reason map
Map<InetAddressAndPort, RequestFailureReason> failures =
CollectionSerializers.deserializeMap(in, version,
InetAddressAndPort.Serializer.inetAddressAndPortSerializer,
RequestFailureReason.serializer);
WriteType writeType = WriteType.serializer.deserialize(in);
return new WriteFailureException(consistency, received, blockFor, writeType, failures);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.WRITE_FAILURE;
}
}

View File

@ -17,8 +17,13 @@
*/
package org.apache.cassandra.exceptions;
import java.io.IOException;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
public class WriteTimeoutException extends RequestTimeoutException
{
@ -36,4 +41,37 @@ public class WriteTimeoutException extends RequestTimeoutException
super(ExceptionCode.WRITE_TIMEOUT, consistency, received, blockFor, msg);
this.writeType = writeType;
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
out.write(consistency.code);
out.writeUnsignedVInt32(received);
out.writeUnsignedVInt32(blockFor);
WriteType.serializer.serialize(writeType, out);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return TypeSizes.BYTE_SIZE + // consistency
TypeSizes.sizeofUnsignedVInt(received) +
TypeSizes.sizeofUnsignedVInt(blockFor) +
WriteType.serializer.serializedSize(writeType);
}
static WriteTimeoutException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte());
int received = in.readUnsignedVInt32();
int blockFor = in.readUnsignedVInt32();
WriteType writeType = WriteType.serializer.deserialize(in);
return new WriteTimeoutException(writeType, consistency, received, blockFor, message);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.WRITE_TIMEOUT;
}
}

View File

@ -26,7 +26,11 @@ import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.utils.Shared;
import static org.apache.cassandra.utils.Shared.Scope.SIMULATION;
@Shared(scope = SIMULATION)
public interface AsymmetricUnversionedSerializer<In, Out>
{
void serialize(In t, DataOutputPlus out) throws IOException;

View File

@ -25,7 +25,11 @@ import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.utils.Shared;
import static org.apache.cassandra.utils.Shared.Scope.SIMULATION;
@Shared(scope = SIMULATION)
public interface IVersionedAsymmetricSerializer<In, Out>
{
/**

View File

@ -21,7 +21,11 @@ import java.io.IOException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.utils.Shared;
import static org.apache.cassandra.utils.Shared.Scope.SIMULATION;
@Shared(scope = SIMULATION)
public interface IVersionedSerializer<T> extends IVersionedAsymmetricSerializer<T, T>
{
static <T> IVersionedSerializer<T> from(UnversionedSerializer<T> delegate)

View File

@ -18,6 +18,11 @@
package org.apache.cassandra.io;
import org.apache.cassandra.utils.Shared;
import static org.apache.cassandra.utils.Shared.Scope.SIMULATION;
@Shared(scope = SIMULATION)
public interface UnversionedSerializer<T> extends AsymmetricUnversionedSerializer<T, T>
{
}

View File

@ -18,13 +18,19 @@
package org.apache.cassandra.locator;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import com.google.common.base.Preconditions;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.IPartitionerDependentSerializer;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ownership.VersionedEndpoints;
@ -167,4 +173,38 @@ public class EndpointsForToken extends Endpoints<EndpointsForToken>
return ClusterMetadata.current().placements.get(keyspace.getMetadata().params.replication).reads.forToken(token);
}
public static final IPartitionerDependentSerializer<EndpointsForToken> serializer = new Serializer();
public static class Serializer implements IPartitionerDependentSerializer<EndpointsForToken>
{
@Override
public void serialize(EndpointsForToken endpoints, DataOutputPlus out, int version) throws IOException
{
Token.compactSerializer.serialize(endpoints.token(), out, version);
out.writeUnsignedVInt32(endpoints.size());
for (Replica replica : endpoints)
Replica.serializer.serialize(replica, out, version);
}
@Override
public EndpointsForToken deserialize(DataInputPlus in, IPartitioner partitioner, int version) throws IOException
{
Token token = Token.compactSerializer.deserialize(in, partitioner, version);
int size = in.readUnsignedVInt32();
EndpointsForToken.Builder builder = EndpointsForToken.builder(token, size);
for (int i = 0; i < size; i++)
builder.add(Replica.serializer.deserialize(in, partitioner, version));
return builder.build();
}
@Override
public long serializedSize(EndpointsForToken endpoints, int version)
{
long size = Token.compactSerializer.serializedSize(endpoints.token(), version);
size += TypeSizes.sizeofUnsignedVInt(endpoints.size());
for (Replica replica : endpoints)
size += Replica.serializer.serializedSize(replica, version);
return size;
}
}
}

View File

@ -754,8 +754,6 @@ public class ReplicaPlans
ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWriteLiveAndDown(metadata, keyspace, tk);
Replicas.temporaryAssertFull(liveAndDown.all()); // TODO CASSANDRA-14547
if (consistencyForPaxos == ConsistencyLevel.LOCAL_SERIAL)
{
// TODO: we should cleanup our semantics here, as we're filtering ALL nodes to localDC which is unexpected for ReplicaPlan

View File

@ -237,12 +237,27 @@ public class Message<T> implements ResponseContext
return out(verb, payload);
}
public static <T> Message<T> out(Verb verb, T payload, boolean isUrgent, long expiresAtNanos)
{
assert !verb.isResponse();
if (isUrgent)
return outWithFlag(verb, payload, MessageFlag.URGENT, expiresAtNanos);
else
return out(verb, payload, expiresAtNanos);
}
public static <T> Message<T> outWithFlag(Verb verb, T payload, MessageFlag flag)
{
assert !verb.isResponse();
return outWithParam(nextId(), verb, 0, payload, flag.addTo(0), null, null);
}
public static <T> Message<T> outWithFlag(Verb verb, T payload, MessageFlag flag, long expiresAtNanos)
{
assert !verb.isResponse();
return outWithParam(nextId(), verb, expiresAtNanos, payload, flag.addTo(0), null, null);
}
public static <T> Message<T> outWithParam(Verb verb, T payload, ParamType paramType, Object paramValue)
{
assert !verb.isResponse() : verb;

View File

@ -121,10 +121,22 @@ import org.apache.cassandra.service.accord.serializers.SetDurableSerializers;
import org.apache.cassandra.service.accord.serializers.Version;
import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState;
import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState.ConsensusKeyMigrationFinished;
import org.apache.cassandra.service.paxos.CasForwardHandler;
import org.apache.cassandra.service.paxos.CasForwardRequest;
import org.apache.cassandra.service.paxos.CasForwardResponse;
import org.apache.cassandra.service.paxos.Commit;
import org.apache.cassandra.service.paxos.Commit.Agreed;
import org.apache.cassandra.service.paxos.ConsensusReadForwardHandler;
import org.apache.cassandra.service.paxos.ConsensusReadForwardRequest;
import org.apache.cassandra.service.paxos.Paxos2CommitForwardHandler;
import org.apache.cassandra.service.paxos.Paxos2CommitForwardRequest;
import org.apache.cassandra.service.paxos.PaxosCommit;
import org.apache.cassandra.service.paxos.PaxosCommitAndPrepare;
import org.apache.cassandra.service.paxos.PaxosCommitForwardHandler;
import org.apache.cassandra.service.paxos.PaxosCommitForwardRequest;
import org.apache.cassandra.service.paxos.PrepareRefreshForwardHandler;
import org.apache.cassandra.service.paxos.PrepareRefreshForwardRequest;
import org.apache.cassandra.service.paxos.PrepareRefreshForwardResponse;
import org.apache.cassandra.service.paxos.PaxosPrepare;
import org.apache.cassandra.service.paxos.PaxosPrepareRefresh;
import org.apache.cassandra.service.paxos.PaxosPropose;
@ -174,6 +186,7 @@ import static org.apache.cassandra.concurrent.Stage.MUTATION;
import static org.apache.cassandra.concurrent.Stage.PAXOS_REPAIR;
import static org.apache.cassandra.concurrent.Stage.READ;
import static org.apache.cassandra.concurrent.Stage.REQUEST_RESPONSE;
import static org.apache.cassandra.concurrent.Stage.TRACKED_CAS;
import static org.apache.cassandra.concurrent.Stage.TRACING;
import static org.apache.cassandra.net.ResponseHandlerSupplier.RESPONSE_HANDLER;
import static org.apache.cassandra.net.Verb.Kind.CUSTOM;
@ -227,6 +240,8 @@ public enum Verb
PAXOS_PROPOSE_REQ (34, P2, writeTimeout, MUTATION, () -> Commit.serializer, () -> ProposeVerbHandler.instance, PAXOS_PROPOSE_RSP ),
PAXOS_COMMIT_RSP (95, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
PAXOS_COMMIT_REQ (35, P2, writeTimeout, MUTATION, () -> Agreed.serializer, () -> PaxosCommit.requestHandler, PAXOS_COMMIT_RSP ),
PAXOS_COMMIT_FORWARD_RSP (96, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
PAXOS_COMMIT_FORWARD_REQ (32, P2, writeTimeout, TRACKED_CAS, () -> PaxosCommitForwardRequest.serializer, () -> PaxosCommitForwardHandler.instance, PAXOS_COMMIT_FORWARD_RSP ),
TRUNCATE_RSP (79, P0, truncateTimeout, REQUEST_RESPONSE, () -> TruncateResponse.serializer, RESPONSE_HANDLER ),
TRUNCATE_REQ (19, P0, truncateTimeout, MUTATION, () -> TruncateRequest.serializer, () -> TruncateVerbHandler.instance, TRUNCATE_RSP ),
@ -308,6 +323,16 @@ public enum Verb
PAXOS2_CLEANUP_COMPLETE_REQ (48, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupComplete.serializer, () -> PaxosCleanupComplete.verbHandler, PAXOS2_CLEANUP_COMPLETE_RSP ),
PAXOS2_UPDATE_LOW_BALLOT_RSP (67, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, RESPONSE_HANDLER ),
PAXOS2_UPDATE_LOW_BALLOT_REQ (64, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosUpdateLowBallot.serializer, () -> PaxosUpdateLowBallot.verbHandler, PAXOS2_UPDATE_LOW_BALLOT_RSP ),
PAXOS2_COMMIT_FORWARD_RSP (71, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ),
PAXOS2_COMMIT_FORWARD_REQ (72, P2, writeTimeout, TRACKED_CAS, () -> Paxos2CommitForwardRequest.serializer, () -> Paxos2CommitForwardHandler.instance, PAXOS2_COMMIT_FORWARD_RSP ),
// CAS and consensus read forwarding for tracked keyspaces
CAS_FORWARD_RSP (73, P2, writeTimeout, REQUEST_RESPONSE, () -> CasForwardResponse.serializer, RESPONSE_HANDLER ),
CAS_FORWARD_REQ (74, P2, writeTimeout, TRACKED_CAS, () -> CasForwardRequest.serializer, () -> CasForwardHandler.instance, CAS_FORWARD_RSP ),
CONSENSUS_READ_FORWARD_RSP (75, P2, readTimeout, REQUEST_RESPONSE, () -> CasForwardResponse.serializer, RESPONSE_HANDLER ),
CONSENSUS_READ_FORWARD_REQ (76, P2, readTimeout, TRACKED_CAS, () -> ConsensusReadForwardRequest.serializer, () -> ConsensusReadForwardHandler.instance, CONSENSUS_READ_FORWARD_RSP ),
PAXOS_PREPARE_REFRESH_FORWARD_RSP(77, P2, writeTimeout, REQUEST_RESPONSE, () -> PrepareRefreshForwardResponse.serializer,RESPONSE_HANDLER ),
PAXOS_PREPARE_REFRESH_FORWARD_REQ(78, P2, writeTimeout, TRACKED_CAS, () -> PrepareRefreshForwardRequest.serializer, () -> PrepareRefreshForwardHandler.instance, PAXOS_PREPARE_REFRESH_FORWARD_RSP),
// transactional cluster metadata
TCM_COMMIT_RSP (801, P0, rpcTimeout, INTERNAL_METADATA, MessageSerializers::commitResultSerializer, RESPONSE_HANDLER ),

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.replication;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Comparator;
import org.apache.cassandra.db.TypeSizes;
@ -113,6 +114,29 @@ public class MutationId extends ShortMutationId
*/
public static final Comparator<MutationId> comparator = ShortMutationId.comparator::compare;
public ByteBuffer toByteBuffer()
{
return ByteBuffer.allocate(16)
.putLong(logId())
.putLong(sequenceId())
.flip();
}
public static MutationId fromByteBuffer(ByteBuffer buffer)
{
if (buffer.remaining() < 16)
throw new IllegalStateException();
int pos = buffer.position();
long logId = buffer.getLong(pos);
long sequenceId = buffer.getLong(pos + 8);
if (logId == MutationId.none().logId() && sequenceId == MutationId.none().sequenceId())
return MutationId.none();
return new MutationId(logId, sequenceId);
}
public static class Serializer implements IVersionedSerializer<MutationId>
{
@Override

View File

@ -398,6 +398,9 @@ public class TrackedWriteRequest
static void applyMutationLocally(Mutation mutation, RequestCallback<NoPayload> handler)
{
Preconditions.checkArgument(handler instanceof TrackedWriteResponseHandler || handler instanceof ForwardedWrite.LeaderCallback);
// TODO: Why execute immediately before even sending the messages to the other nodes?
// Also this is already going to be on the mutation stage and the mutation stage can get backed up so don't we want
// to start on a stage that is less likley to get backed up?
Stage.MUTATION.maybeExecuteImmediately(new LocalMutationRunnable(mutation, handler));
}

View File

@ -1,61 +0,0 @@
/*
* 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.service;
import accord.primitives.Txn;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.SinglePartitionReadCommand;
import org.apache.cassandra.db.partitions.FilteredPartition;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.service.accord.txn.TxnResult;
import org.apache.cassandra.service.paxos.Ballot;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.transport.Dispatcher;
import static org.apache.cassandra.service.StorageProxy.ConsensusAttemptResult;
/**
* Abstract the conditions and updates for a CAS operation.
*/
public interface CASRequest
{
Dispatcher.RequestTime requestTime();
/**
* The command to use to fetch the value to compare for the CAS.
*/
SinglePartitionReadCommand readCommand(long nowInSec);
/**
* Returns whether the provided CF, that represents the values fetched using the
* readFilter(), match the CAS conditions this object stands for.
*/
boolean appliesTo(FilteredPartition current) throws InvalidRequestException;
/**
* The updates to perform of a CAS success. The values fetched using the readFilter()
* are passed as argument.
*/
PartitionUpdate makeUpdates(FilteredPartition current, ClientState clientState, Ballot ballot) throws InvalidRequestException;
Txn toAccordTxn(ClusterMetadata cm, ConsistencyLevel consistencyLevel, ConsistencyLevel commitConsistencyLevel, ClientState clientState, long nowInSecs);
ConsensusAttemptResult toCasResult(TxnResult txnResult);
}

View File

@ -195,6 +195,13 @@ public class ClientState
this.remoteAddress = null;
}
private ClientState(boolean isInternal, boolean applyGuardrails)
{
this.isInternal = isInternal;
this.applyGuardrails = applyGuardrails;
this.remoteAddress = null;
}
protected ClientState(InetSocketAddress remoteAddress)
{
this.isInternal = false;
@ -237,6 +244,21 @@ public class ClientState
return new ClientState((InetSocketAddress)remoteAddress);
}
/**
* @return a minimal ClientState for forwarded tracked Paxos requests (for guardrail application)
*/
public static ClientState forForwardedCalls(boolean isInternal, boolean applyGuardrails, boolean isSuper)
{
return new ClientState(isInternal, applyGuardrails)
{
@Override
public boolean isSuper()
{
return isSuper;
}
};
}
/**
* Clone this ClientState object, but use the provided keyspace instead of the
* keyspace in this ClientState object.

View File

@ -57,6 +57,7 @@ import org.slf4j.LoggerFactory;
import accord.primitives.Txn;
import org.agrona.collections.IntHashSet;
import org.apache.cassandra.batchlog.Batch;
import org.apache.cassandra.batchlog.BatchlogManager;
import org.apache.cassandra.concurrent.DebuggableTask.RunnableDebuggableTask;
@ -65,6 +66,7 @@ import org.apache.cassandra.config.AccordSpec;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.statements.CQL3CasRequest;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.CounterMutation;
@ -72,6 +74,7 @@ import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.EmptyIterators;
import org.apache.cassandra.db.IMutation;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.KeyspaceNotDefinedException;
import org.apache.cassandra.db.MessageParams;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition;
@ -96,6 +99,7 @@ import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.CasWriteTimeoutException;
import org.apache.cassandra.exceptions.CasWriteUnknownResultException;
import org.apache.cassandra.exceptions.CassandraException;
import org.apache.cassandra.exceptions.CoordinatorBehindException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.IsBootstrappingException;
@ -112,6 +116,7 @@ import org.apache.cassandra.exceptions.UnavailableException;
import org.apache.cassandra.exceptions.WriteFailureException;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.hints.Hint;
import org.apache.cassandra.hints.HintsService;
import org.apache.cassandra.locator.AbstractReplicationStrategy;
@ -132,9 +137,13 @@ import org.apache.cassandra.net.ForwardingInfo;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessageFlag;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.NoPayload;
import org.apache.cassandra.net.RequestCallback;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.replication.TrackedWriteRequest;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.PartitionDenylist;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
@ -164,9 +173,13 @@ import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.C
import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.SplitReads;
import org.apache.cassandra.service.consensus.migration.TransactionalMigrationFromMode;
import org.apache.cassandra.service.paxos.Ballot;
import org.apache.cassandra.service.paxos.CasForwardRequest;
import org.apache.cassandra.service.paxos.CasForwardResponse;
import org.apache.cassandra.service.paxos.Commit;
import org.apache.cassandra.service.paxos.ConsensusReadForwardRequest;
import org.apache.cassandra.service.paxos.ContentionStrategy;
import org.apache.cassandra.service.paxos.Paxos;
import org.apache.cassandra.service.paxos.PaxosCommitForwardRequest;
import org.apache.cassandra.service.paxos.PaxosState;
import org.apache.cassandra.service.paxos.v1.PrepareCallback;
import org.apache.cassandra.service.paxos.v1.ProposeCallback;
@ -189,13 +202,17 @@ import org.apache.cassandra.utils.NoSpamLogger;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.Throwables;
import org.apache.cassandra.utils.TimeUUID;
import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.apache.cassandra.utils.concurrent.CountDownLatch;
import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.Promise;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static accord.primitives.Txn.Kind.Read;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.concat;
import static java.util.Collections.singleton;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.db.ConsistencyLevel.SERIAL;
@ -210,8 +227,10 @@ import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.writeMetr
import static org.apache.cassandra.net.Message.out;
import static org.apache.cassandra.net.NoPayload.noPayload;
import static org.apache.cassandra.net.Verb.BATCH_STORE_REQ;
import static org.apache.cassandra.net.Verb.CONSENSUS_READ_FORWARD_REQ;
import static org.apache.cassandra.net.Verb.MUTATION_REQ;
import static org.apache.cassandra.net.Verb.PAXOS_COMMIT_REQ;
import static org.apache.cassandra.net.Verb.PAXOS_COMMIT_FORWARD_REQ;
import static org.apache.cassandra.net.Verb.PAXOS_PREPARE_REQ;
import static org.apache.cassandra.net.Verb.PAXOS_PROPOSE_REQ;
import static org.apache.cassandra.net.Verb.SCHEMA_VERSION_REQ;
@ -250,6 +269,7 @@ public class StorageProxy implements StorageProxyMBean
public static final String UNREACHABLE = "UNREACHABLE";
private static final int FAILURE_LOGGING_INTERVAL_SECONDS = CassandraRelevantProperties.FAILURE_LOGGING_INTERVAL_SECONDS.getInt();
private static final boolean DISABLE_CONSENSUS_REQUEST_FORWARDING = CassandraRelevantProperties.DISABLE_CONSENSUS_REQUEST_FORWARDING.getBoolean();
private static final String UNSAFE_MIXED_MUTATIONS_MSG = "Mutations look to have different time sources, some are using 'USING TIMESTAMP' and others are using the server timestamp; writes to the Accord table will not be linearizable while using transactions. To allow this behavior set accord.mixed_time_source_handling=log or ignore";
private static final WritePerformer standardWritePerformer;
@ -367,13 +387,48 @@ public class StorageProxy implements StorageProxyMBean
public static RowIterator cas(String keyspaceName,
String cfName,
DecoratedKey key,
CASRequest request,
CQL3CasRequest request,
ConsistencyLevel consistencyForPaxos,
ConsistencyLevel consistencyForCommit,
ClientState clientState,
long nowInSeconds,
Dispatcher.RequestTime requestTime)
throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException, CasWriteUnknownResultException
{
return casInternal(keyspaceName, cfName, key, request, consistencyForPaxos, consistencyForCommit,
clientState, nowInSeconds, requestTime, false);
}
/**
* Version of cas called by handlers that have already received a forwarded request.
* This prevents infinite forwarding loops if the forwarding target is not actually a replica.
*/
public static RowIterator casForwarded(String keyspaceName,
String cfName,
DecoratedKey key,
CQL3CasRequest request,
ConsistencyLevel consistencyForPaxos,
ConsistencyLevel consistencyForCommit,
ClientState clientState,
long nowInSeconds,
Dispatcher.RequestTime requestTime)
throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException, CasWriteUnknownResultException
{
return casInternal(keyspaceName, cfName, key, request, consistencyForPaxos, consistencyForCommit,
clientState, nowInSeconds, requestTime, true);
}
private static RowIterator casInternal(String keyspaceName,
String cfName,
DecoratedKey key,
CQL3CasRequest request,
ConsistencyLevel consistencyForPaxos,
ConsistencyLevel consistencyForCommit,
ClientState clientState,
long nowInSeconds,
Dispatcher.RequestTime requestTime,
boolean alreadyForwarded)
throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException, CasWriteUnknownResultException
{
if (DatabaseDescriptor.getPartitionDenylistEnabled() && DatabaseDescriptor.getDenylistWritesEnabled() && !partitionDenylist.isKeyPermitted(keyspaceName, cfName, key.getKey()))
{
@ -382,6 +437,13 @@ public class StorageProxy implements StorageProxyMBean
key, keyspaceName, cfName));
}
// Check if this CAS operation needs to be forwarded to a replica coordinator for tracked keyspaces
RowIterator forwardResult = checkAndForwardCasIfNeeded(keyspaceName, cfName, key, request,
consistencyForPaxos, consistencyForCommit,
clientState, nowInSeconds, requestTime, alreadyForwarded);
if (forwardResult != null)
return forwardResult;
ConsensusAttemptResult lastAttemptResult = null;
do
{
@ -432,7 +494,7 @@ public class StorageProxy implements StorageProxyMBean
private static ConsensusAttemptResult legacyCas(TableMetadata metadata,
DecoratedKey key,
CASRequest request,
CQL3CasRequest request,
ConsistencyLevel consistencyForPaxos,
ConsistencyLevel consistencyForCommit,
ClientState clientState,
@ -628,7 +690,7 @@ public class StorageProxy implements StorageProxyMBean
// because we also skip replaying those same empty update in beginAndRepairPaxos (see the longer
// comment there). As empty update are somewhat common (serial reads and non-applying CAS propose
// them), this is worth bothering.
if (!proposal.update.isEmpty())
if (!proposal.isEmpty())
commitPaxos(proposal, consistencyForCommit, true, requestTime);
RowIterator result = proposalPair.right;
if (result != null)
@ -733,7 +795,7 @@ public class StorageProxy implements StorageProxyMBean
// replayed in that case.
// Tl;dr, it is safe to skip committing empty updates _as long as_ we also skip replying them below. And
// doing is more efficient, so we do so.
if (!inProgress.update.isEmpty() && inProgress.isAfter(mostRecent))
if (!inProgress.isEmpty() && inProgress.isAfter(mostRecent))
{
Tracing.trace("Finishing incomplete paxos round {}", inProgress);
casMetrics.unfinishedCommit.inc();
@ -760,7 +822,7 @@ public class StorageProxy implements StorageProxyMBean
if (Iterables.size(missingMRC) > 0)
{
Tracing.trace("Repairing replicas that missed the most recent commit");
sendCommit(mostRecent, missingMRC);
sendCommit(mostRecent, missingMRC, paxosPlan);
// TODO: provided commits don't invalid the prepare we just did above (which they don't), we could just wait
// for all the missingMRC to acknowledge this commit and then move on with proposing our value. But that means
// adding the ability to have commitPaxos block, which is exactly CASSANDRA-5442 will do. So once we have that
@ -781,19 +843,108 @@ public class StorageProxy implements StorageProxyMBean
}
/**
* Unlike commitPaxos, this does not wait for replies
* Unlike commitPaxos, this does not wait for replies.
* For tracked keyspaces, ensures proper mutation ID generation and tracking.
*/
private static void sendCommit(Commit commit, Iterable<InetAddressAndPort> replicas)
private static void sendCommit(Commit commit, Iterable<InetAddressAndPort> targetReplicas, ReplicaPlan.ForPaxosWrite replicaPlan)
{
Message<Commit> message = Message.out(PAXOS_COMMIT_REQ, commit);
for (InetAddressAndPort target : replicas)
MessagingService.instance().send(message, target);
String ksName = commit.metadata().keyspace;
KeyspaceMetadata ksMetadata = Schema.instance.getKeyspaceMetadata(ksName);
// Non-tracked keyspaces OR already has mutation ID: fire and forget to target replicas
if (ksMetadata == null || !ksMetadata.params.replicationType.isTracked() || !commit.mutation.id().isNone())
{
Message<Commit> message = Message.out(PAXOS_COMMIT_REQ, commit);
for (InetAddressAndPort target : targetReplicas)
MessagingService.instance().send(message, target);
return;
}
// No mutation ID - need to generate one
InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort();
boolean localIsReplica = replicaPlan.liveAndDown().endpoints().contains(localEndpoint);
if (localIsReplica)
{
// Local node is a replica - use commitPaxosTracked (generates ID, sends to ALL replicas)
Keyspace keyspace = Keyspace.open(ksName);
commitPaxosTracked(keyspace, commit, replicaPlan.consistencyLevel(),
Dispatcher.RequestTime.forImmediateExecution(), true);
}
else
{
// Local node is not a replica - forward to a replica coordinator
forwardPaxosCommit(commit, replicaPlan);
}
}
/**
* Forwards a Paxos commit to a replica coordinator for tracked keyspaces.
* The coordinator will generate mutation ID and send to ALL replicas.
* This method waits for the coordinator to confirm it has sent the commits.
*/
private static void forwardPaxosCommit(Commit commit, ReplicaPlan.ForPaxosWrite replicaPlan)
{
InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort();
// Get live replicas (excluding local node) to find a coordinator
EndpointsForToken liveReplicas = replicaPlan.live().filter(replica -> !replica.endpoint().equals(localEndpoint));
if (liveReplicas.isEmpty())
{
logger.warn("No live replicas available to forward Paxos commit for tracked keyspace");
return;
}
// Sort by proximity and select the closest as coordinator
EndpointsForToken sortedReplicas = DatabaseDescriptor.getNodeProximity().sortedByProximity(localEndpoint, liveReplicas);
InetAddressAndPort replicaCoordinator = sortedReplicas.get(0).endpoint();
Tracing.trace("Forwarding Paxos commit to replica coordinator {}", replicaCoordinator);
// Use respondAfterSend=true so coordinator responds after sending commits (not waiting for application)
PaxosCommitForwardRequest forwardRequest = new PaxosCommitForwardRequest(commit, replicaPlan.consistencyLevel(), true);
Message<PaxosCommitForwardRequest> message = Message.out(PAXOS_COMMIT_FORWARD_REQ, forwardRequest);
// Wait for coordinator to confirm commits were sent before returning
Promise<NoPayload> promise = new AsyncPromise<>();
RequestCallback<NoPayload> callback = new RequestCallback<NoPayload>()
{
@Override
public void onResponse(Message<NoPayload> response)
{
promise.setSuccess(response.payload);
}
@Override
public void onFailure(InetAddressAndPort from, RequestFailure reason)
{
promise.setFailure(new RuntimeException("Failed to forward Paxos commit to " + from + ": " + reason));
}
};
MessagingService.instance().sendWithCallback(message, replicaCoordinator, callback);
try
{
// Wait for coordinator to confirm commits were sent
promise.get(DatabaseDescriptor.getWriteRpcTimeout(MILLISECONDS), MILLISECONDS);
}
catch (TimeoutException e)
{
logger.warn("Timeout waiting for forwarded Paxos commit response from {}", replicaCoordinator);
}
catch (Exception e)
{
logger.warn("Error waiting for forwarded Paxos commit response from {}", replicaCoordinator, e);
}
}
private static PrepareCallback preparePaxos(Commit toPrepare, ReplicaPlan.ForPaxosWrite replicaPlan, Dispatcher.RequestTime requestTime)
throws WriteTimeoutException
{
PrepareCallback callback = new PrepareCallback(toPrepare.update.partitionKey(), toPrepare.update.metadata(), replicaPlan.requiredParticipants(), replicaPlan.consistencyLevel(), requestTime);
PrepareCallback callback = new PrepareCallback(toPrepare.partitionKey(), toPrepare.metadata(), replicaPlan.requiredParticipants(), replicaPlan.consistencyLevel(), requestTime);
Message<Commit> message = Message.out(PAXOS_PREPARE_REQ, toPrepare);
boolean hasLocalRequest = false;
@ -873,10 +1024,139 @@ public class StorageProxy implements StorageProxyMBean
private static void commitPaxos(Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints, Dispatcher.RequestTime requestTime) throws WriteTimeoutException
{
boolean shouldBlock = consistencyLevel != ConsistencyLevel.ANY;
Keyspace keyspace = Keyspace.open(proposal.update.metadata().keyspace);
checkArgument(!proposal.isEmpty());
// Check if this is a tracked keyspace
String keyspaceName = proposal.metadata().keyspace;
Keyspace keyspace = Keyspace.openIfExists(keyspaceName);
if (keyspace == null)
throw new KeyspaceNotDefinedException("Keyspace " + keyspaceName + " does not exist");
KeyspaceMetadata ksMetadata = keyspace.getMetadata();
if (ksMetadata.params.replicationType.isTracked())
{
// For tracked keyspaces, check if we need to forward or execute directly
Token tk = proposal.partitionKey().getToken();
ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(keyspace, consistencyLevel, tk, ReplicaPlans.writeAll);
if (isTrackedKeyspaceRequiringPaxosCommitForwarding(ksMetadata, proposal, replicaPlan.liveAndDown()))
{
// Forward to a replica coordinator
forwardPaxosCommit(proposal, consistencyLevel, replicaPlan);
}
else
{
// Execute directly using tracked logic
commitPaxosTracked(keyspace, proposal, consistencyLevel, requestTime);
}
}
else
{
// For untracked keyspaces, use existing logic
commitPaxosUntracked(keyspace, proposal, consistencyLevel, allowHints, requestTime);
}
}
Token tk = proposal.update.partitionKey().getToken();
public static void commitPaxosTracked(Keyspace keyspace, Commit proposal, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) throws WriteTimeoutException
{
commitPaxosTracked(keyspace, proposal, consistencyLevel, requestTime, false);
}
/**
* Commit a Paxos proposal for a tracked keyspace.
* Generates a mutation ID, sends to all replicas, and optionally waits for responses.
*
* @param respondAfterSend if true, returns immediately after sending (don't wait for application)
*/
public static void commitPaxosTracked(Keyspace keyspace, Commit proposal, ConsistencyLevel consistencyLevel,
Dispatcher.RequestTime requestTime, boolean respondAfterSend) throws WriteTimeoutException
{
boolean shouldBlock = !respondAfterSend && consistencyLevel != ConsistencyLevel.ANY;
String keyspaceName = proposal.metadata().keyspace;
Token tk = proposal.partitionKey().getToken();
// Generate mutation ID for tracked keyspace, preserving the commit subclass
MutationId mutationId;
if (proposal.mutation.id().isNone())
{
mutationId = MutationTrackingService.instance.nextMutationId(keyspaceName, tk);
proposal = proposal.withMutationId(mutationId);
}
else
{
// A commit that is being resent might already have an ID read out of the system table
mutationId = proposal.mutation.id();
}
// NOTE: this ReplicaPlan is a lie, this usage of ReplicaPlan could do with being clarified - the selected() collection is essentially (I think) never used
ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(keyspace, consistencyLevel, tk, ReplicaPlans.writeAll);
AbstractReplicationStrategy rs = replicaPlan.replicationStrategy();
// If we are coordinating a new mutation id for the first time then create a TrackedWriteResponseHandler
AbstractWriteResponseHandler<?> responseHandler = rs.getWriteResponseHandler(replicaPlan, null, WriteType.SIMPLE, null, requestTime);
responseHandler = TrackedWriteResponseHandler.wrap(responseHandler, mutationId);
// For tracked keyspaces, the local commit MUST execute synchronously BEFORE sending to remote replicas.
// This ensures the mutation is written to the journal before any failure callback can trigger
// reconciliation via ActiveLogReconciler.
Message<Commit> message = Message.outWithFlag(PAXOS_COMMIT_REQ, proposal, MessageFlag.CALL_BACK_ON_FAILURE);
Replica localReplica = null;
for (Replica replica : replicaPlan.liveAndDown())
{
if (replica.isSelf())
{
localReplica = replica;
break;
}
}
// Execute local commit SYNCHRONOUSLY first to ensure journal write completes
if (localReplica != null)
{
try
{
PaxosState.commitDirect(proposal);
if (shouldBlock)
responseHandler.onResponse(null);
}
catch (Exception ex)
{
if (!(ex instanceof WriteTimeoutException))
logger.error("Failed to apply paxos commit locally", ex);
if (shouldBlock)
responseHandler.onFailure(FBUtilities.getBroadcastAddressAndPort(), RequestFailure.forException(ex));
}
}
// Now send to remote replicas
IntHashSet remoteReplicas = new IntHashSet();
for (Replica replica : replicaPlan.liveAndDown())
{
if (replica.isSelf())
continue; // Already executed locally above
InetAddressAndPort destination = replica.endpoint();
remoteReplicas.add(ClusterMetadata.current().directory.peerId(destination).id());
if (shouldBlock)
MessagingService.instance().sendWriteWithCallback(message, replica, responseHandler);
else
MessagingService.instance().send(message, destination);
}
// Register write request with tracking service
if (!remoteReplicas.isEmpty())
MutationTrackingService.instance.sentWriteRequest(proposal.makeMutation(), remoteReplicas);
if (shouldBlock)
responseHandler.get();
}
private static void commitPaxosUntracked(Keyspace keyspace, Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints, Dispatcher.RequestTime requestTime) throws WriteTimeoutException
{
boolean shouldBlock = consistencyLevel != ConsistencyLevel.ANY;
PartitionUpdate update = proposal.update;
Token tk = update.partitionKey().getToken();
AbstractWriteResponseHandler<Commit> responseHandler = null;
// NOTE: this ReplicaPlan is a lie, this usage of ReplicaPlan could do with being clarified - the selected() collection is essentially (I think) never used
@ -963,6 +1243,148 @@ public class StorageProxy implements StorageProxyMBean
});
}
/**
* Checks if this commit needs to be forwarded to a replica coordinator for tracked keyspace support.
*/
private static boolean isTrackedKeyspaceRequiringPaxosCommitForwarding(KeyspaceMetadata ksMetadata, Commit proposal, EndpointsForToken participants)
{
if (!ksMetadata.params.replicationType.isTracked())
return false;
// Check if current coordinator is not a replica
InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort();
boolean isLocalReplica = participants.endpoints().contains(localEndpoint);
return !isLocalReplica;
}
/**
* Forwards a Paxos V1 commit operation to a replica coordinator for tracked keyspaces.
* Uses the replica plan to select the best live, non-local replica based on proximity.
*/
private static void forwardPaxosCommit(Commit proposal, ConsistencyLevel consistencyLevel, ReplicaPlan.ForWrite replicaPlan) throws WriteTimeoutException
{
InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort();
// Get live replicas and filter out local node
EndpointsForToken liveReplicas = replicaPlan.live().filter(replica -> !replica.endpoint().equals(localEndpoint));
if (liveReplicas.isEmpty())
{
// No live replica available, throw exception
throw new WriteTimeoutException(WriteType.CAS, consistencyLevel, 0, consistencyLevel.blockFor(replicaPlan.replicationStrategy()));
}
// Sort by proximity and select the best coordinator
EndpointsForToken sortedReplicas = DatabaseDescriptor.getNodeProximity().sortedByProximity(localEndpoint, liveReplicas);
InetAddressAndPort replicaCoordinator = sortedReplicas.get(0).endpoint();
// Create forward request with participant list
PaxosCommitForwardRequest forwardRequest = new PaxosCommitForwardRequest(proposal, consistencyLevel);
Message<PaxosCommitForwardRequest> message = Message.out(PAXOS_COMMIT_FORWARD_REQ, forwardRequest);
// Use AsyncPromise for proper callback handling
Promise<NoPayload> promise = new AsyncPromise<>();
RequestCallback<NoPayload> callback = new RequestCallback<NoPayload>()
{
@Override
public void onResponse(Message<NoPayload> response)
{
promise.setSuccess(response.payload);
}
@Override
public void onFailure(InetAddressAndPort from, RequestFailure reason)
{
promise.setFailure(new WriteTimeoutException(WriteType.CAS, consistencyLevel, 0, consistencyLevel.blockFor(replicaPlan.replicationStrategy())));
}
};
try
{
MessagingService.instance().sendWithCallback(message, replicaCoordinator, callback);
// Wait for response with timeout
promise.get(DatabaseDescriptor.getWriteRpcTimeout(java.util.concurrent.TimeUnit.MILLISECONDS), java.util.concurrent.TimeUnit.MILLISECONDS);
}
catch (TimeoutException e)
{
throw new WriteTimeoutException(WriteType.CAS, consistencyLevel, 0, consistencyLevel.blockFor(replicaPlan.replicationStrategy()));
}
catch (Exception e)
{
if (e instanceof WriteTimeoutException)
throw (WriteTimeoutException) e;
throw new WriteTimeoutException(WriteType.CAS, consistencyLevel, 0, consistencyLevel.blockFor(replicaPlan.replicationStrategy()));
}
}
/**
* Performs a coordinated write with mutation tracking.
* Assumes that local coordinator is a replica (forwarding implementation pending).
*
* @param mutation
* @param consistencyLevel
* @param requestTime
*/
public static void mutateWithTracking(Mutation mutation, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
try
{
TrackedWriteRequest.perform(mutation, consistencyLevel, requestTime).get();
}
catch (WriteTimeoutException|WriteFailureException ex)
{
if (consistencyLevel == ConsistencyLevel.ANY)
{
// TODO (expected): what exactly?
}
else
{
if (ex instanceof WriteFailureException)
{
writeMetrics.failures.mark();
writeMetricsForLevel(consistencyLevel).failures.mark();
WriteFailureException fe = (WriteFailureException)ex;
Tracing.trace("Write failure; received {} of {} required replies, failed {} requests",
fe.received, fe.blockFor, fe.failureReasonByEndpoint.size());
}
else
{
writeMetrics.timeouts.mark();
writeMetricsForLevel(consistencyLevel).timeouts.mark();
WriteTimeoutException te = (WriteTimeoutException)ex;
Tracing.trace("Write timeout; received {} of {} required replies", te.received, te.blockFor);
}
throw ex;
}
}
catch (UnavailableException e)
{
writeMetrics.unavailables.mark();
writeMetricsForLevel(consistencyLevel).unavailables.mark();
Tracing.trace("Unavailable");
throw e;
}
catch (OverloadedException e)
{
writeMetrics.unavailables.mark();
writeMetricsForLevel(consistencyLevel).unavailables.mark();
Tracing.trace("Overloaded");
throw e;
}
finally
{
// We track latency based on request processing time, since the amount of time that request spends in the queue
// is not a representative metric of replica performance.
long latency = nanoTime() - requestTime.startedAtNanos();
writeMetrics.addNano(latency);
writeMetricsForLevel(consistencyLevel).addNano(latency);
updateCoordinatorWriteLatencyTableMetric(singleton(mutation), latency);
}
}
/**
* Use this method to have these Mutations applied
* across all replicas. This method will take care
@ -2258,9 +2680,30 @@ public class StorageProxy implements StorageProxyMBean
isForWrite);
}
private static PartitionIterator readWithConsensus(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
public static PartitionIterator readWithConsensus(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
throws InvalidRequestException, UnavailableException, ReadFailureException, ReadTimeoutException
{
return readWithConsensusInternal(group, consistencyLevel, requestTime, false);
}
/**
* Version of readWithConsensus called by handlers that have already received a forwarded request.
* This prevents infinite forwarding loops if the forwarding target is not actually a replica.
*/
public static PartitionIterator readWithConsensusForwarded(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
throws InvalidRequestException, UnavailableException, ReadFailureException, ReadTimeoutException
{
return readWithConsensusInternal(group, consistencyLevel, requestTime, true);
}
private static PartitionIterator readWithConsensusInternal(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime, boolean alreadyForwarded)
throws InvalidRequestException, UnavailableException, ReadFailureException, ReadTimeoutException
{
// Check if this consensus read needs to be forwarded to a replica coordinator for tracked keyspaces
PartitionIterator forwardResult = checkAndForwardConsensusReadIfNeeded(group, consistencyLevel, requestTime, alreadyForwarded);
if (forwardResult != null)
return forwardResult;
ConsensusAttemptResult lastResult;
do
{
@ -3611,6 +4054,16 @@ public class StorageProxy implements StorageProxyMBean
{
return new ConsensusAttemptResult(casResult, null, false);
}
/**
* Get the CAS result row iterator.
* @return the CAS result, or null if this was not a CAS operation result
*/
@Nullable
public RowIterator getCasResult()
{
return casResult;
}
}
@Override
@ -3878,4 +4331,191 @@ public class StorageProxy implements StorageProxyMBean
{
DatabaseDescriptor.setClientRequestSizeMetricsEnabled(enabled);
}
/**
* Check if a CAS operation needs to be forwarded to a replica coordinator for tracked keyspaces.
* Returns null if no forwarding is needed, or the result of the forwarded operation.
*/
private static RowIterator checkAndForwardCasIfNeeded(String keyspaceName,
String cfName,
DecoratedKey key,
CQL3CasRequest request,
ConsistencyLevel consistencyForPaxos,
ConsistencyLevel consistencyForCommit,
ClientState clientState,
long nowInSeconds,
Dispatcher.RequestTime requestTime,
boolean alreadyForwarded)
throws UnavailableException, RequestFailureException, RequestTimeoutException
{
// Get keyspace metadata to check if it's tracked
Keyspace keyspace = Keyspace.openIfExists(keyspaceName);
if (keyspace == null)
throw new KeyspaceNotDefinedException("Keyspace " + keyspaceName + " does not exist");
KeyspaceMetadata ksMetadata = keyspace.getMetadata();
if (!ksMetadata.params.replicationType.isTracked())
return null; // Not tracked, no forwarding needed
// Property to disable top-level forwarding for testing
if (DISABLE_CONSENSUS_REQUEST_FORWARDING)
return null;
// Check if current coordinator is not a replica
Token tk = key.getToken();
EndpointsForToken allReplicas = ReplicaLayout.forTokenWriteLiveAndDown(ClusterMetadata.current(), keyspace, tk)
.all();
EndpointsForToken liveReplicas = allReplicas.filter(FailureDetector.isReplicaAlive);
InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort();
boolean isLocalReplica = allReplicas.contains(localEndpoint);
if (isLocalReplica)
return null; // Local node is a replica, no forwarding needed
// If this request was already forwarded to us and we're not a replica, something is wrong
if (alreadyForwarded)
{
logger.error("Received forwarded CAS for keyspace {} table {} key {} but local node {} is not a replica. Replicas are: {}",
keyspaceName, cfName, key, localEndpoint, allReplicas);
Tracing.trace("ERROR: Received forwarded CAS but local node is not a replica");
throw new InvalidRequestException("Forwarded CAS received by non-replica node " + localEndpoint);
}
// Find best replica to forward to using proximity-based selection
if (liveReplicas.isEmpty())
throw new UnavailableException("No live replicas available for CAS forwarding", consistencyForPaxos, 1, 0);
// Sort by proximity and select the best coordinator
EndpointsForToken sortedReplicas = DatabaseDescriptor.getNodeProximity().sortedByProximity(localEndpoint, liveReplicas);
InetAddressAndPort replicaCoordinator = sortedReplicas.get(0).endpoint();
// Create forward request
CasForwardRequest forwardRequest =
new CasForwardRequest(keyspaceName, cfName, key, consistencyForPaxos, consistencyForCommit,
nowInSeconds, clientState, request);
Message<CasForwardRequest> message = Message.out(Verb.CAS_FORWARD_REQ, forwardRequest);
try
{
// Send synchronous request to replica coordinator
Object responseObj = MessagingService.instance().sendWithResult(message, replicaCoordinator).get();
@SuppressWarnings("unchecked")
Message<CasForwardResponse> responseMessage = (Message<CasForwardResponse>) responseObj;
CasForwardResponse response = responseMessage.payload;
// Add warnings from forwarded operation to local ClientWarn
for (String warning : response.warnings)
ClientWarn.instance.warn(warning);
// Check if the forwarded operation had an exception
if (!response.isSuccess())
{
throw response.exception;
}
return response.result;
}
catch (CassandraException ce)
{
// Rethrow CassandraExceptions from the replica coordinator
throw ce;
}
catch (Exception e)
{
throw new RuntimeException("Failed to forward CAS operation to replica coordinator", e);
}
}
/**
* Check if a consensus read operation needs to be forwarded to a replica coordinator for tracked keyspaces.
* Returns null if no forwarding is needed, or the result of the forwarded operation.
*/
private static PartitionIterator checkAndForwardConsensusReadIfNeeded(SinglePartitionReadCommand.Group group,
ConsistencyLevel consistencyLevel,
Dispatcher.RequestTime requestTime,
boolean alreadyForwarded)
throws UnavailableException, ReadFailureException, ReadTimeoutException
{
if (group.queries.isEmpty())
return null;
// Use the first command to determine keyspace and key for replica planning
SinglePartitionReadCommand firstCommand = group.queries.get(0);
String keyspaceName = firstCommand.metadata().keyspace;
// Get keyspace metadata to check if it's tracked
Keyspace keyspace = Keyspace.openIfExists(keyspaceName);
if (keyspace == null)
throw new KeyspaceNotDefinedException("Keyspace " + keyspaceName + " does not exist");
KeyspaceMetadata ksMetadata = keyspace.getMetadata();
if (!ksMetadata.params.replicationType.isTracked())
return null; // Not tracked, no forwarding needed
// Property to disable top-level forwarding for testing
if (DISABLE_CONSENSUS_REQUEST_FORWARDING)
return null;
// Check if current coordinator is not a replica
Token tk = firstCommand.partitionKey().getToken();
EndpointsForToken allReplicas = ReplicaLayout.forTokenWriteLiveAndDown(ClusterMetadata.current(), keyspace, tk)
.all();
EndpointsForToken liveReplicas = allReplicas.filter(FailureDetector.isReplicaAlive);
InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort();
boolean isLocalReplica = allReplicas.contains(localEndpoint);
if (isLocalReplica)
return null; // Local node is a replica, no forwarding needed
// If this request was already forwarded to us and we're not a replica, something is wrong
if (alreadyForwarded)
{
logger.error("Received forwarded consensus read for keyspace {} key {} but local node {} is not a replica. Replicas are: {}",
keyspaceName, firstCommand.partitionKey(), localEndpoint, allReplicas);
Tracing.trace("ERROR: Received forwarded consensus read but local node is not a replica");
throw new RuntimeException("Forwarded consensus read received by non-replica node " + localEndpoint);
}
// Find best replica to forward to using proximity-based selection
if (liveReplicas.isEmpty())
throw new UnavailableException("No live replicas available for consensus read forwarding", consistencyLevel, 1, 0);
// Sort by proximity and select the best coordinator
EndpointsForToken sortedReplicas = DatabaseDescriptor.getNodeProximity().sortedByProximity(localEndpoint, liveReplicas);
InetAddressAndPort replicaCoordinator = sortedReplicas.get(0).endpoint();
// Create forward request - consensus reads only have a single command
ConsensusReadForwardRequest forwardRequest = new ConsensusReadForwardRequest(firstCommand, consistencyLevel);
Message<ConsensusReadForwardRequest> message = Message.out(CONSENSUS_READ_FORWARD_REQ, forwardRequest);
try
{
// Send synchronous request to replica coordinator
Object responseObj = MessagingService.instance().sendWithResult(message, replicaCoordinator).get();
@SuppressWarnings("unchecked")
Message<CasForwardResponse> responseMessage = (Message<CasForwardResponse>) responseObj;
CasForwardResponse response = responseMessage.payload;
// Add warnings from forwarded operation to local ClientWarn
for (String warning : response.warnings)
ClientWarn.instance.warn(warning);
// Check if the forwarded operation had an exception
if (!response.isSuccess())
throw response.exception;
return response.partitionIterator();
}
catch (CassandraException ce)
{
// Rethrow CassandraExceptions from the replica coordinator
throw ce;
}
catch (Exception e)
{
throw new RuntimeException("Failed to forward consensus read operation to replica coordinator", e);
}
}
}

View File

@ -19,37 +19,37 @@ package org.apache.cassandra.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.WriteFailureException;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.NoPayload;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.replication.MutationTrackingService;
public class TrackedWriteResponseHandler extends AbstractWriteResponseHandler<NoPayload>
public class TrackedWriteResponseHandler<T> extends AbstractWriteResponseHandler<T>
{
private static final Logger logger = LoggerFactory.getLogger(TrackedWriteResponseHandler.class);
private final AbstractWriteResponseHandler<NoPayload> wrapped;
private final AbstractWriteResponseHandler<T> wrapped;
private final MutationId mutationId;
private TrackedWriteResponseHandler(AbstractWriteResponseHandler<NoPayload> wrapped, MutationId mutationId)
private TrackedWriteResponseHandler(AbstractWriteResponseHandler<T> wrapped, MutationId mutationId)
{
super(wrapped.replicaPlan, wrapped.callback, wrapped.writeType, null, wrapped.getRequestTime());
this.wrapped = wrapped;
this.mutationId = mutationId;
}
public static TrackedWriteResponseHandler wrap(AbstractWriteResponseHandler<NoPayload> handler, MutationId mutationId)
public static <T> TrackedWriteResponseHandler<T> wrap(AbstractWriteResponseHandler<T> handler, MutationId mutationId)
{
return new TrackedWriteResponseHandler(handler, mutationId);
return new TrackedWriteResponseHandler<>(handler, mutationId);
}
@Override
public void onResponse(Message<NoPayload> msg)
public void onResponse(Message<T> msg)
{
// Local mutations are witnessed from Keyspace.applyInternalTracked
if (msg != null)

View File

@ -18,7 +18,13 @@
package org.apache.cassandra.service.accord.exceptions;
import java.io.IOException;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.exceptions.CassandraExceptionCode;
import org.apache.cassandra.exceptions.ReadTimeoutException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import static org.apache.cassandra.db.ConsistencyLevel.SERIAL;
@ -33,4 +39,35 @@ public class AccordReadExhaustedException extends ReadTimeoutException
{
super(SERIAL, received, blockFor, dataPresent, msg);
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
// Only serialize the fields that vary - consistency is always SERIAL
out.writeUnsignedVInt32(received);
out.writeUnsignedVInt32(blockFor);
out.writeBoolean(dataPresent);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return TypeSizes.sizeofUnsignedVInt(received) +
TypeSizes.sizeofUnsignedVInt(blockFor) +
TypeSizes.BOOL_SIZE; // dataPresent
}
public static AccordReadExhaustedException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
int received = in.readUnsignedVInt32();
int blockFor = in.readUnsignedVInt32();
boolean dataPresent = in.readBoolean();
return new AccordReadExhaustedException(received, blockFor, dataPresent, message);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.ACCORD_READ_EXHAUSTED;
}
}

View File

@ -18,7 +18,13 @@
package org.apache.cassandra.service.accord.exceptions;
import java.io.IOException;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.exceptions.CassandraExceptionCode;
import org.apache.cassandra.exceptions.ReadTimeoutException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import static org.apache.cassandra.db.ConsistencyLevel.SERIAL;
@ -34,4 +40,35 @@ public class AccordReadPreemptedException extends ReadTimeoutException
{
super(SERIAL, received, blockFor, dataPresent, msg);
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
// Only serialize the fields that vary - consistency is always SERIAL
out.writeUnsignedVInt32(received);
out.writeUnsignedVInt32(blockFor);
out.writeBoolean(dataPresent);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return TypeSizes.sizeofUnsignedVInt(received) +
TypeSizes.sizeofUnsignedVInt(blockFor) +
TypeSizes.BOOL_SIZE; // dataPresent
}
public static AccordReadPreemptedException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
int received = in.readUnsignedVInt32();
int blockFor = in.readUnsignedVInt32();
boolean dataPresent = in.readBoolean();
return new AccordReadPreemptedException(received, blockFor, dataPresent, message);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.ACCORD_READ_PREEMPTED;
}
}

View File

@ -18,8 +18,14 @@
package org.apache.cassandra.service.accord.exceptions;
import java.io.IOException;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.exceptions.CassandraExceptionCode;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import static org.apache.cassandra.db.ConsistencyLevel.SERIAL;
@ -34,4 +40,32 @@ public class AccordWriteExhaustedException extends WriteTimeoutException
{
super(WriteType.CAS, SERIAL, received, blockFor, msg);
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
// Only serialize the fields that vary - consistency is always SERIAL, writeType is always CAS
out.writeUnsignedVInt32(received);
out.writeUnsignedVInt32(blockFor);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return TypeSizes.sizeofUnsignedVInt(received) +
TypeSizes.sizeofUnsignedVInt(blockFor);
}
public static AccordWriteExhaustedException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
int received = in.readUnsignedVInt32();
int blockFor = in.readUnsignedVInt32();
return new AccordWriteExhaustedException(received, blockFor, message);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.ACCORD_WRITE_EXHAUSTED;
}
}

View File

@ -18,8 +18,14 @@
package org.apache.cassandra.service.accord.exceptions;
import java.io.IOException;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.exceptions.CassandraExceptionCode;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import static org.apache.cassandra.db.ConsistencyLevel.SERIAL;
@ -35,4 +41,32 @@ public class AccordWritePreemptedException extends WriteTimeoutException
{
super(WriteType.CAS, SERIAL, received, blockFor, msg);
}
@Override
protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException
{
// Only serialize the fields that vary - consistency is always SERIAL, writeType is always CAS
out.writeUnsignedVInt32(received);
out.writeUnsignedVInt32(blockFor);
}
@Override
protected long serializedSizeSpecificFields(int version)
{
return TypeSizes.sizeofUnsignedVInt(received) +
TypeSizes.sizeofUnsignedVInt(blockFor);
}
public static AccordWritePreemptedException deserializeFields(String message, DataInputPlus in, int version) throws IOException
{
int received = in.readUnsignedVInt32();
int blockFor = in.readUnsignedVInt32();
return new AccordWritePreemptedException(received, blockFor, message);
}
@Override
public CassandraExceptionCode getCassandraExceptionCode()
{
return CassandraExceptionCode.ACCORD_WRITE_PREEMPTED;
}
}

View File

@ -60,13 +60,17 @@ public class TxnReferenceOperations
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TxnReferenceOperations that = (TxnReferenceOperations) o;
if (this.isEmpty() && that.isEmpty())
return true;
return metadata.equals(that.metadata) && clusterings.equals(that.clusterings) && regulars.equals(that.regulars) && statics.equals(that.statics);
}
@Override
public int hashCode()
{
return Objects.hash(metadata, clusterings, regulars, statics);
return Objects.hash(regulars, statics);
}
@Override
@ -85,12 +89,35 @@ public class TxnReferenceOperations
return regulars.isEmpty() && statics.isEmpty();
}
/**
* Public accessors for CAS forwarding serialization support.
* These enable reuse of TxnReferenceOperations structure without exposing internal implementation.
*/
public List<Clustering<?>> getClusterings()
{
return clusterings;
}
public List<TxnReferenceOperation> getRegulars()
{
return regulars;
}
public List<TxnReferenceOperation> getStatics()
{
return statics;
}
static final ParameterisedVersionedSerializer<TxnReferenceOperations, TableMetadatas, Version> serializer = new ParameterisedVersionedSerializer<>()
{
private static final int HAS_CONTENT = 0x01;
@Override
public void serialize(TxnReferenceOperations operations, TableMetadatas tables, DataOutputPlus out, Version version) throws IOException
{
out.writeBoolean(!operations.isEmpty());
int flags = (!operations.isEmpty() ? HAS_CONTENT : 0);
out.write(flags);
if (operations.isEmpty())
return;
@ -105,7 +132,10 @@ public class TxnReferenceOperations
@Override
public TxnReferenceOperations deserialize(TableMetadatas tables, DataInputPlus in, Version version) throws IOException
{
if (!in.readBoolean())
int flags = in.readUnsignedByte();
boolean hasContent = (flags & HAS_CONTENT) != 0;
if (!hasContent)
return TxnReferenceOperations.empty();
TableMetadata metadata = tables.deserialize(in);
@ -120,7 +150,7 @@ public class TxnReferenceOperations
@Override
public long serializedSize(TxnReferenceOperations operations, TableMetadatas tables, Version version)
{
long size = TypeSizes.BOOL_SIZE;
long size = TypeSizes.BYTE_SIZE; // flags byte
if (operations.isEmpty())
return size;
size += tables.serializedSize(operations.metadata);
@ -128,19 +158,8 @@ public class TxnReferenceOperations
for (Clustering<?> clustering : operations.clusterings)
size += Clustering.serializer.serializedSize(clustering, version.messageVersion(), operations.metadata.comparator.subtypes());
size += serializedListSize(operations.regulars, tables, TxnReferenceOperation.serializer);
size += serializedListSize(operations.statics, tables, TxnReferenceOperation.serializer);
size += serializedListSize(operations.statics, tables, TxnReferenceOperation.serializer);
return size;
}
private TableMetadatas tables(TxnReferenceOperations operations)
{
TableMetadatas.Collector collector = new TableMetadatas.Collector();
collector.add(operations.metadata);
for (TxnReferenceOperation op : operations.regulars)
op.collect(collector);
for (TxnReferenceOperation op : operations.statics)
op.collect(collector);
return collector.build();
}
};
}

View File

@ -24,7 +24,9 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@ -47,16 +49,23 @@ import accord.utils.SimpleBitSets;
import accord.utils.async.AsyncChain;
import accord.utils.async.AsyncChains;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.UpdateParameters;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.Columns;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.DeletionTime;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.RangeTombstone;
import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts;
import org.apache.cassandra.db.RegularAndStaticColumns;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.marshal.TimeUUIDType;
import org.apache.cassandra.db.partitions.FilteredPartition;
import org.apache.cassandra.db.partitions.Partition;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.ParameterisedVersionedSerializer;
import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.io.util.DataInputPlus;
@ -64,15 +73,19 @@ import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.accord.AccordCommandStore;
import org.apache.cassandra.service.accord.AccordExecutor;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.accord.serializers.TableMetadatas;
import org.apache.cassandra.service.accord.serializers.TableMetadatasAndKeys;
import org.apache.cassandra.service.accord.serializers.Version;
import org.apache.cassandra.service.paxos.Ballot;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.ObjectSizes;
import org.apache.cassandra.utils.SimpleBitSetSerializers;
import org.apache.cassandra.utils.TimeUUID;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.cassandra.db.rows.DeserializationHelper.Flag.FROM_REMOTE;
@ -324,6 +337,56 @@ public class TxnWrite extends AbstractKeySorted<TxnWrite.Update> implements Writ
return new Update(this.key, index, updateBuilder.build(), tables);
}
public void completeToBuilder(PartitionUpdate.Builder updateBuilder, TxnData data, Ballot ballot, FilteredPartition current, ClientState clientState) throws InvalidRequestException
{
if (isComplete())
{
// No reference operations - add base update components directly
if (!baseUpdate.staticRow().isEmpty())
updateBuilder.add(baseUpdate.staticRow());
for (Row row : baseUpdate)
updateBuilder.add(row);
// Copy deletion info (partition deletion and range tombstones)
// Timestamps will be updated later in Proposal.of() via updateAllTimestamp()
DeletionTime partitionDeletion = baseUpdate.deletionInfo().getPartitionDeletion();
if (!partitionDeletion.isLive())
updateBuilder.addPartitionDeletion(partitionDeletion);
Iterator<RangeTombstone> rangeIterator = baseUpdate.deletionInfo().rangeIterator(false);
while (rangeIterator.hasNext())
updateBuilder.add(rangeIterator.next());
return;
}
// Create CAS-specific UpdateParameters with prefetched data and transaction timestamp
long transactionTimestamp = ballot.unixMicros();
Map<DecoratedKey, Partition> prefetchedRows = Collections.singletonMap(
baseUpdate.partitionKey(), current);
CasUpdateParameters up = new CasUpdateParameters(
baseUpdate.metadata(), clientState, QueryOptions.DEFAULT,
transactionTimestamp, (int)(transactionTimestamp / 1000), 0,
prefetchedRows, ballot.msb(), 0);
// Apply static operations
Row staticRow = applyUpdates(baseUpdate.staticRow(), referenceOps.getStatics(),
baseUpdate.partitionKey(), Clustering.STATIC_CLUSTERING, up, data);
if (!staticRow.isEmpty())
updateBuilder.add(staticRow);
// Apply regular operations
for (Clustering<?> clustering : referenceOps.clusterings)
{
Row existing = baseUpdate.hasRows() ? baseUpdate.getRow(clustering) : null;
Row row = applyUpdates(existing, referenceOps.regulars, baseUpdate.partitionKey(), clustering, up, data);
if (row != null)
updateBuilder.add(row);
}
}
private static Columns columns(Columns current, List<TxnReferenceOperation> referenceOps)
{
if (referenceOps.isEmpty())
@ -370,8 +433,8 @@ public class TxnWrite extends AbstractKeySorted<TxnWrite.Update> implements Writ
return up.buildRow();
}
static final FragmentSerializer serializer = new FragmentSerializer();
static class FragmentSerializer
public static final FragmentSerializer serializer = new FragmentSerializer();
public static class FragmentSerializer
{
public void serialize(Fragment fragment, TableMetadatas tables, DataOutputPlus out, Version version) throws IOException
{
@ -433,6 +496,31 @@ public class TxnWrite extends AbstractKeySorted<TxnWrite.Update> implements Writ
}
}
/**
* CAS-specific UpdateParameters for CAS-specific TimeUUID handling
*/
private static class CasUpdateParameters extends UpdateParameters
{
private final long timeUuidMsb;
private long timeUuidNanos;
public CasUpdateParameters(TableMetadata metadata, ClientState state, QueryOptions options,
long timestamp, long nowInSec, int ttl,
Map<DecoratedKey, Partition> prefetchedRows,
long timeUuidMsb, long timeUuidNanos) throws InvalidRequestException
{
super(metadata, state, options, timestamp, nowInSec, ttl, prefetchedRows);
this.timeUuidMsb = timeUuidMsb;
this.timeUuidNanos = timeUuidNanos;
}
@Override
public byte[] nextTimeUUIDAsBytes()
{
return TimeUUID.toBytes(timeUuidMsb, TimeUUIDType.signedBytesToNativeLong(timeUuidNanos++));
}
}
public final TableMetadatas tables;
private final SimpleBitSet conditionalBlockBitSet;

View File

@ -0,0 +1,124 @@
/*
* 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.service.paxos;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.exceptions.CassandraException;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
/**
* Handler for forwarded CAS (Compare-And-Set) operations.
* Executes the CAS operation on behalf of the original coordinator,
* ensuring that MutationId generation happens on a replica coordinator for tracked keyspaces.
*
* TODO (expected): more comprehensive testing
*/
public class CasForwardHandler implements IVerbHandler<CasForwardRequest>
{
public static final CasForwardHandler instance = new CasForwardHandler();
private static final Logger logger = LoggerFactory.getLogger(CasForwardHandler.class);
@Override
public void doVerb(Message<CasForwardRequest> message)
{
CasForwardRequest request = message.payload;
Tracing.trace("Executing forwarded CAS operation for {}", request.key);
// Start capturing client warnings for the forwarded operation
ClientWarn.instance.captureWarnings();
try
{
// Validate keyspace exists and is tracked
KeyspaceMetadata ksMetadata = Schema.instance.getKeyspaceMetadata(request.keyspaceName);
if (ksMetadata == null)
{
MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message);
logger.error("Failed to forward CAS operation for non-existent keyspace {}", request.keyspaceName);
return;
}
if (!ksMetadata.params.replicationType.isTracked())
{
MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message);
logger.error("Asked to perform forwarded CAS operation, but keyspace {} is not tracked", request.keyspaceName);
return;
}
// Execute the forwarded CAS operation
logger.debug("Executing CAS operation for table {}.{} with key {}",
request.keyspaceName, request.cfName, request.key);
// Execute the CAS request using StorageProxy with the forwarded client state
Dispatcher.RequestTime requestTime = Dispatcher.RequestTime.forImmediateExecution();
RowIterator result = StorageProxy.casForwarded(request.keyspaceName,
request.cfName,
request.key,
request.casRequest,
request.consistencyForPaxos,
request.consistencyForCommit,
request.clientState,
request.nowInSeconds,
requestTime);
// Create response with the CAS result and captured warnings
List<String> warnings = ClientWarn.instance.getWarnings();
CasForwardResponse response = new CasForwardResponse(result, warnings);
MessagingService.instance().respond(response, message);
logger.debug("Completed forwarded CAS operation for {}", request.key);
}
catch (CassandraException ce)
{
// Forward the exception back to the original coordinator with warnings
List<String> warnings = ClientWarn.instance.getWarnings();
CasForwardResponse response = new CasForwardResponse(ce, warnings);
MessagingService.instance().respond(response, message);
}
catch (Throwable t)
{
try
{
MessagingService.instance().respondWithFailure(RequestFailure.forException(t), message);
}
finally
{
throw t;
}
}
finally
{
ClientWarn.instance.resetWarnings();
}
}
}

View File

@ -0,0 +1,140 @@
/*
* 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.service.paxos;
import java.io.IOException;
import org.apache.cassandra.cql3.statements.CQL3CasRequest;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.service.ClientState;
/**
* Request to forward a CAS operation to a replica coordinator for tracked keyspaces.
* Contains the essential information needed to execute the CAS operation on the remote coordinator.
*/
public class CasForwardRequest
{
public static final Serializer serializer = new Serializer();
public final String keyspaceName;
public final String cfName;
public final DecoratedKey key;
public final ConsistencyLevel consistencyForPaxos;
public final ConsistencyLevel consistencyForCommit;
public final long nowInSeconds;
public final ClientState clientState;
public final CQL3CasRequest casRequest; // The actual CAS request to forward
public CasForwardRequest(String keyspaceName,
String cfName,
DecoratedKey key,
ConsistencyLevel consistencyForPaxos,
ConsistencyLevel consistencyForCommit,
long nowInSeconds,
ClientState clientState,
CQL3CasRequest casRequest)
{
this.keyspaceName = keyspaceName;
this.cfName = cfName;
this.key = key;
this.consistencyForPaxos = consistencyForPaxos;
this.consistencyForCommit = consistencyForCommit;
this.nowInSeconds = nowInSeconds;
this.clientState = clientState;
this.casRequest = casRequest;
}
public static class Serializer implements IVersionedSerializer<CasForwardRequest>
{
@Override
public void serialize(CasForwardRequest forwardRequest, DataOutputPlus out, int version) throws IOException
{
out.writeUTF(forwardRequest.keyspaceName);
out.writeUTF(forwardRequest.cfName);
DecoratedKey.serializer.serialize(forwardRequest.key, out, version);
out.writeByte(forwardRequest.consistencyForPaxos.code);
out.writeByte(forwardRequest.consistencyForCommit.code);
out.writeUnsignedVInt(forwardRequest.nowInSeconds);
serializeClientState(forwardRequest.clientState, out);
CQL3CasRequest.serializer.serialize(forwardRequest.casRequest, out, version);
}
@Override
public CasForwardRequest deserialize(DataInputPlus in, int version) throws IOException
{
String keyspaceName = in.readUTF();
String cfName = in.readUTF();
DecoratedKey key = (DecoratedKey) DecoratedKey.serializer.deserialize(in, version);
ConsistencyLevel consistencyForPaxos = ConsistencyLevel.fromCode(in.readUnsignedByte());
ConsistencyLevel consistencyForCommit = ConsistencyLevel.fromCode(in.readUnsignedByte());
long nowInSeconds = in.readUnsignedVInt();
ClientState clientState = deserializeClientState(in);
CQL3CasRequest casRequest = CQL3CasRequest.serializer.deserialize(in, version);
return new CasForwardRequest(keyspaceName, cfName, key, consistencyForPaxos, consistencyForCommit,
nowInSeconds, clientState, casRequest);
}
@Override
public long serializedSize(CasForwardRequest forwardRequest, int version)
{
long size = 0;
size += TypeSizes.sizeof(forwardRequest.keyspaceName);
size += TypeSizes.sizeof(forwardRequest.cfName);
size += DecoratedKey.serializer.serializedSize(forwardRequest.key, version);
size += 1; // consistencyForPaxos.code
size += 1; // consistencyForCommit.code
size += TypeSizes.sizeofUnsignedVInt(forwardRequest.nowInSeconds);
size += serializedSizeClientState(forwardRequest.clientState);
size += CQL3CasRequest.serializer.serializedSize(forwardRequest.casRequest, version);
return size;
}
private static final int IS_SUPER = 0x01;
private static final int IS_INTERNAL = 0x02;
private static final int APPLY_GUARDRAILS = 0x04;
private static void serializeClientState(ClientState state, DataOutputPlus out) throws IOException
{
int flags = (state.isSuper() ? IS_SUPER : 0)
| (state.isInternal ? IS_INTERNAL : 0)
| (state.applyGuardrails() ? APPLY_GUARDRAILS : 0)
;
out.write(flags);
}
private static ClientState deserializeClientState(DataInputPlus in) throws IOException
{
int flags = in.readUnsignedByte();
boolean isSuper = (flags & IS_SUPER) != 0;
boolean isInternal = (flags & IS_INTERNAL) != 0;
boolean applyGuardrails = (flags & APPLY_GUARDRAILS) != 0;
return ClientState.forForwardedCalls(isInternal, applyGuardrails, isSuper);
}
private static long serializedSizeClientState(ClientState state)
{
return 1;
}
}
}

View File

@ -0,0 +1,182 @@
/*
* 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.service.paxos;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.partitions.FilteredPartition;
import org.apache.cassandra.db.partitions.PartitionIterator;
import org.apache.cassandra.db.partitions.PartitionIterators;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.db.rows.UnfilteredRowIteratorSerializer;
import org.apache.cassandra.db.rows.UnfilteredRowIterators;
import org.apache.cassandra.exceptions.CassandraException;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.CollectionSerializers;
import static org.apache.cassandra.db.SerializationHeader.StableHeaderSerializer.STABLE;
import static org.apache.cassandra.db.rows.DeserializationHelper.Flag.FROM_REMOTE;
/**
* Response containing the result of a forwarded CAS operation.
* Can contain either a successful result or an exception that occurred during execution.
*/
public class CasForwardResponse
{
public final RowIterator result;
public final CassandraException exception;
@Nonnull
public final List<String> warnings;
public CasForwardResponse(RowIterator result, List<String> warnings)
{
this(result, null, warnings);
}
public CasForwardResponse(PartitionIterator result, List<String> warnings)
{
// Extract the single partition from the iterator (consensus reads are single partition)
this(result != null && result.hasNext() ? result.next() : null, null, warnings);
}
public CasForwardResponse(CassandraException exception, List<String> warnings)
{
this(null, exception, warnings);
}
private CasForwardResponse(RowIterator result, CassandraException exception, List<String> warnings)
{
this.result = result;
this.exception = exception;
this.warnings = warnings == null ? Collections.emptyList() : warnings;
}
public boolean isSuccess()
{
return exception == null;
}
/**
* Get the result as a PartitionIterator.
*/
public PartitionIterator partitionIterator()
{
return result == null ? null : PartitionIterators.singletonIterator(result);
}
public static final Serializer serializer = new Serializer();
public static class Serializer implements IVersionedSerializer<CasForwardResponse>
{
private static final int HAS_RESULT = 0x01;
private static final int HAS_EXCEPTION = 0x02;
private static final int HAS_WARNINGS = 0x04;
@Override
public void serialize(CasForwardResponse response, DataOutputPlus out, int version) throws IOException
{
int flags = (response.result != null ? HAS_RESULT : 0)
| (response.exception != null ? HAS_EXCEPTION : 0)
| (!response.warnings.isEmpty() ? HAS_WARNINGS : 0)
;
out.write(flags);
if (response.result != null)
{
FilteredPartition partition = new FilteredPartition(response.result);
partition.metadata().id.serializeCompact(out);
try (UnfilteredRowIterator iterator = partition.unfilteredIterator())
{
UnfilteredRowIteratorSerializer.serializer.serialize(iterator, out, version, partition.rowCount(), STABLE, null);
}
}
if (response.exception != null)
CassandraException.serializer.serialize(response.exception, out, version);
if (!response.warnings.isEmpty())
CollectionSerializers.serializeList(response.warnings, out, version, CollectionSerializers.STRING_SERIALIZER);
}
@Override
public CasForwardResponse deserialize(DataInputPlus in, int version) throws IOException
{
int flags = in.readUnsignedByte();
boolean hasResult = (flags & HAS_RESULT) != 0;
boolean hasException = (flags & HAS_EXCEPTION) != 0;
boolean hasWarnings = (flags & HAS_WARNINGS) != 0;
RowIterator result = null;
if (hasResult)
{
TableMetadata metadata = Schema.instance.getExistingTableMetadata(TableId.deserializeCompact(in));
UnfilteredRowIteratorSerializer.Header header = UnfilteredRowIteratorSerializer.serializer.deserializeHeader(metadata, in, version, FROM_REMOTE, STABLE, null);
try (UnfilteredRowIterator partition = UnfilteredRowIteratorSerializer.serializer.deserialize(in, version, metadata, FROM_REMOTE, header))
{
result = UnfilteredRowIterators.filter(partition, 0);
}
}
CassandraException exception = null;
if (hasException)
exception = CassandraException.serializer.deserialize(in, version);
List<String> warnings = Collections.emptyList();
if (hasWarnings)
warnings = CollectionSerializers.deserializeList(in, version, CollectionSerializers.STRING_SERIALIZER);
return new CasForwardResponse(result, exception, warnings);
}
@Override
public long serializedSize(CasForwardResponse response, int version)
{
long size = TypeSizes.BYTE_SIZE; // flags byte
if (response.result != null)
{
FilteredPartition partition = new FilteredPartition(response.result);
size += partition.metadata().id.serializedCompactSize();
try (UnfilteredRowIterator iterator = partition.unfilteredIterator())
{
size += UnfilteredRowIteratorSerializer.serializer.serializedSize(iterator, version, partition.rowCount(), STABLE, null);
}
}
if (response.exception != null)
size += CassandraException.serializer.serializedSize(response.exception, version);
if (!response.warnings.isEmpty())
size += CollectionSerializers.serializedListSize(response.warnings, version, CollectionSerializers.STRING_SERIALIZER);
return size;
}
}
}

View File

@ -33,9 +33,11 @@ import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.DeserializationHelper;
import org.apache.cassandra.db.rows.EncodingStats;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableMetadata;
@ -51,17 +53,22 @@ public class Commit
{
enum CompareResult { SAME, BEFORE, AFTER, IS_REPROPOSAL, WAS_REPROPOSED_BY}
public static final CommitSerializer<Commit> serializer = new CommitSerializer<>(Commit::new);
public static final CommitSerializer<Commit> serializer = new CommitSerializer<>(Commit::new, Commit::new);
public static class Proposal extends Commit
{
public static final CommitSerializer<Proposal> serializer = new CommitSerializer<>(Proposal::new);
public static final CommitSerializer<Proposal> serializer = new CommitSerializer<>(Proposal::new, Proposal::new);
public Proposal(Ballot ballot, PartitionUpdate update)
{
super(ballot, update);
}
public Proposal(Ballot ballot, Mutation mutation)
{
super(ballot, mutation);
}
public String toString()
{
return toString("Proposal");
@ -80,18 +87,24 @@ public class Commit
public Accepted accepted()
{
return new Accepted(ballot, update);
return new Accepted(ballot, mutation);
}
public Agreed agreed()
{
return new Agreed(ballot, update);
return new Agreed(ballot, mutation);
}
@Override
public Proposal withMutationId(MutationId mutationId)
{
return new Proposal(ballot, makeMutation(mutationId));
}
}
public static class Accepted extends Proposal
{
public static final CommitSerializer<Accepted> serializer = new CommitSerializer<>(Accepted::new);
public static final CommitSerializer<Accepted> serializer = new CommitSerializer<>(Accepted::new, Accepted::new);
public static Accepted none(DecoratedKey partitionKey, TableMetadata metadata)
{
@ -103,14 +116,19 @@ public class Commit
super(ballot, update);
}
public Accepted(Ballot ballot, Mutation mutation)
{
super(ballot, mutation);
}
public Accepted(Commit commit)
{
super(commit.ballot, commit.update);
super(commit.ballot, commit.mutation);
}
Committed committed()
{
return new Committed(ballot, update);
return new Committed(ballot, mutation);
}
boolean isExpired(long nowInSec)
@ -133,13 +151,19 @@ public class Commit
return c > 0 ? a : b;
return a instanceof AcceptedWithTTL ? ((AcceptedWithTTL)a).lastDeleted(b) : a;
}
@Override
public Accepted withMutationId(MutationId mutationId)
{
return new Accepted(ballot, makeMutation(mutationId));
}
}
public static class AcceptedWithTTL extends Accepted
{
public static AcceptedWithTTL withDefaultTTL(Commit copy)
{
return new AcceptedWithTTL(copy, nowInSeconds() + legacyPaxosTtlSec(copy.update.metadata()));
return new AcceptedWithTTL(copy, nowInSeconds() + legacyPaxosTtlSec(copy.metadata()));
}
public final long localDeletionTime;
@ -166,27 +190,44 @@ public class Commit
return b instanceof AcceptedWithTTL && localDeletionTime >= ((AcceptedWithTTL) b).localDeletionTime
? this : b;
}
@Override
public AcceptedWithTTL withMutationId(MutationId mutationId)
{
return new AcceptedWithTTL(ballot, makeMutation(mutationId).getOnlyUpdate(), localDeletionTime);
}
}
// might prefer to call this Commit, but would mean refactoring more legacy code
public static class Agreed extends Accepted
{
public static final CommitSerializer<Agreed> serializer = new CommitSerializer<>(Agreed::new);
public static final CommitSerializer<Agreed> serializer = new CommitSerializer<>(Agreed::new, Agreed::new);
public Agreed(Ballot ballot, PartitionUpdate update)
{
super(ballot, update);
}
public Agreed(Ballot ballot, Mutation mutation)
{
super(ballot, mutation);
}
public Agreed(Commit copy)
{
super(copy);
}
@Override
public Agreed withMutationId(MutationId mutationId)
{
return new Agreed(ballot, makeMutation(mutationId));
}
}
public static class Committed extends Agreed
{
public static final CommitSerializer<Committed> serializer = new CommitSerializer<>(Committed::new);
public static final CommitSerializer<Committed> serializer = new CommitSerializer<>(Committed::new, Committed::new);
public static Committed none(DecoratedKey partitionKey, TableMetadata metadata)
{
@ -198,6 +239,11 @@ public class Commit
super(ballot, update);
}
public Committed(Ballot ballot, Mutation mutation)
{
super(ballot, mutation);
}
public Committed(Commit copy)
{
super(copy);
@ -220,13 +266,19 @@ public class Commit
{
return ballot.equals(Ballot.none()) && update.isEmpty();
}
@Override
public Committed withMutationId(MutationId mutationId)
{
return new Committed(ballot, makeMutation(mutationId));
}
}
public static class CommittedWithTTL extends Committed
{
public static CommittedWithTTL withDefaultTTL(Commit copy)
{
return new CommittedWithTTL(copy, nowInSeconds() + legacyPaxosTtlSec(copy.update.metadata()));
return new CommittedWithTTL(copy, nowInSeconds() + legacyPaxosTtlSec(copy.metadata()));
}
public final long localDeletionTime;
@ -237,6 +289,12 @@ public class Commit
this.localDeletionTime = localDeletionTime;
}
public CommittedWithTTL(Ballot ballot, Mutation mutation, long localDeletionTime)
{
super(ballot, mutation);
this.localDeletionTime = localDeletionTime;
}
public CommittedWithTTL(Commit copy, long localDeletionTime)
{
super(copy);
@ -253,20 +311,62 @@ public class Commit
return b instanceof CommittedWithTTL && localDeletionTime >= ((CommittedWithTTL) b).localDeletionTime
? this : b;
}
@Override
public CommittedWithTTL withMutationId(MutationId mutationId)
{
return new CommittedWithTTL(ballot, makeMutation(mutationId), localDeletionTime);
}
}
public interface Commitable
{
enum CommitableKind
{
PARTITION_UPDATE, MUTATION
}
CommitableKind commitableKind();
}
public final Ballot ballot;
public final Mutation mutation;
public final PartitionUpdate update;
public Commit(Ballot ballot, PartitionUpdate update)
public static Commit create(Ballot ballot, Commitable commitable)
{
switch (commitable.commitableKind())
{
case MUTATION:
return new Commit(ballot, (Mutation)commitable);
case PARTITION_UPDATE:
return new Commit(ballot, (PartitionUpdate) commitable);
default:
throw new AssertionError("Unexpected commitableKind: " + commitable.commitableKind());
}
}
private Commit(Ballot ballot, PartitionUpdate update)
{
assert ballot != null;
assert update != null;
this.ballot = ballot;
this.mutation = new Mutation(MutationId.none(), update, PotentialTxnConflicts.ALLOW);
this.update = update;
}
private Commit(Ballot ballot, Mutation mutation)
{
assert ballot != null;
assert mutation != null;
assert mutation.getPartitionUpdates().size() == 1 : "Paxos commits should only have one partition update";
this.ballot = ballot;
this.mutation = mutation;
this.update = mutation.getOnlyUpdate();
}
public static Commit newPrepare(DecoratedKey partitionKey, TableMetadata metadata, Ballot ballot)
{
return new Commit(ballot, PartitionUpdate.emptyUpdate(metadata, partitionKey));
@ -317,15 +417,45 @@ public class Commit
public Mutation makeMutation()
{
// TODO (expected): what's the best thing to do here? Deriving the mutation id from the ballot seems like the best
// thing to do, like we do with the partition update timestamps, but there are caveats related to id collisions
// and the assumption that a mutation id is unique amonth other mutations, which is not the case w/ paxos ballots,
// which only need to be unique to a given partition key to be accepted. It may be best to keep them separate anyway,
// since the reconciliation process as currently planned will not allow writes with ids before some point in time,
// and there might be edge cases where that locks up paxos execution or has other side effects. The downside of
// not making paxos mutation ids deterministic is that the same commit may create multiple mutation ids if a paxos
// operation is not fully committed, then re-committed on repair or the next operation
return new Mutation(MutationId.fixme(), update, PotentialTxnConflicts.ALLOW);
return mutation;
}
public Mutation makeMutation(MutationId mutationId)
{
if (mutationId == null || mutation.id().equals(mutationId))
return mutation;
PartitionUpdate update = mutation.getOnlyUpdate();
return new Mutation(mutationId, update, mutation.potentialTxnConflicts());
}
/**
* Creates a copy of this Commit with a new mutation ID.
* Subclasses override to preserve their concrete type.
*/
public Commit withMutationId(MutationId mutationId)
{
return new Commit(ballot, makeMutation(mutationId));
}
public DecoratedKey partitionKey()
{
return update.partitionKey();
}
public TableMetadata metadata()
{
return update.metadata();
}
public EncodingStats stats()
{
return update.stats();
}
public boolean isEmpty()
{
return mutation.getOnlyUpdate().isEmpty();
}
@Override
@ -377,7 +507,7 @@ public class Commit
// the timestamp of a mutation stays unchanged as we repropose it, so the timestamp of the mutation
// is the timestamp of the ballot that originally proposed it
long originalBallotOfNewer = newer.update.stats().minTimestamp;
long originalBallotOfNewer = newer.stats().minTimestamp;
// so, if the mutation and ballot timestamps match, this is not a reproposal but a first proposal
if (ballotOfNewer == originalBallotOfNewer)
@ -388,7 +518,7 @@ public class Commit
return true;
// otherwise, it could be that both are reproposals, so just check both for the "original" ballot timestamp
return originalBallotOfNewer == older.update.stats().minTimestamp;
return originalBallotOfNewer == older.stats().minTimestamp;
}
public CompareResult compareWith(Commit that)
@ -488,29 +618,64 @@ public class Commit
public static class CommitSerializer<T extends Commit> implements IVersionedSerializer<T>
{
final BiFunction<Ballot, PartitionUpdate, T> constructor;
public CommitSerializer(BiFunction<Ballot, PartitionUpdate, T> constructor)
final BiFunction<Ballot, PartitionUpdate, T> partitionUpdateConstructor;
final BiFunction<Ballot, Mutation, T> mutationConstructor;
public CommitSerializer(BiFunction<Ballot, PartitionUpdate, T> partitionUpdateConstructor, BiFunction<Ballot, Mutation, T> mutationConstructor)
{
this.constructor = constructor;
this.partitionUpdateConstructor = partitionUpdateConstructor;
this.mutationConstructor = mutationConstructor;
}
public void serialize(T commit, DataOutputPlus out, int version) throws IOException
{
commit.ballot.serialize(out);
PartitionUpdate.serializer.serialize(commit.update, out, version);
// Use version-aware serialization
if (version >= MessagingService.VERSION_61)
{
// New format: serialize Mutation directly
Mutation.serializer.serialize(commit.mutation, out, version);
}
else
{
// Legacy format: serialize PartitionUpdate
PartitionUpdate.serializer.serialize(commit.update, out, version);
}
}
public T deserialize(DataInputPlus in, int version) throws IOException
{
Ballot ballot = Ballot.deserialize(in);
PartitionUpdate update = PartitionUpdate.serializer.deserialize(in, version, DeserializationHelper.Flag.LOCAL);
return constructor.apply(ballot, update);
if (version >= MessagingService.VERSION_61)
{
// New format: deserialize Mutation
Mutation mutation = org.apache.cassandra.db.Mutation.serializer.deserialize(in, version);
return mutationConstructor.apply(ballot, mutation);
}
else
{
// Legacy format: always PartitionUpdate
PartitionUpdate update = PartitionUpdate.serializer.deserialize(in, version, DeserializationHelper.Flag.LOCAL);
return partitionUpdateConstructor.apply(ballot, update);
}
}
public long serializedSize(T commit, int version)
{
return Ballot.sizeInBytes()
+ PartitionUpdate.serializer.serializedSize(commit.update, version);
long size = Ballot.sizeInBytes();
if (version >= MessagingService.VERSION_61)
{
size += Mutation.serializer.serializedSize(commit.mutation, version);
}
else
{
size += PartitionUpdate.serializer.serializedSize(commit.update, version);
}
return size;
}
}

View File

@ -0,0 +1,122 @@
/*
* 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.service.paxos;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.SinglePartitionReadCommand;
import org.apache.cassandra.db.partitions.PartitionIterator;
import org.apache.cassandra.exceptions.CassandraException;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
/**
* Handler for forwarded consensus read operations.
* Executes the consensus read operation on behalf of the original coordinator,
* ensuring proper coordination for tracked keyspaces on a replica coordinator.
*
* TODO (expected): more comprehensive testing
*/
public class ConsensusReadForwardHandler implements IVerbHandler<ConsensusReadForwardRequest>
{
public static final ConsensusReadForwardHandler instance = new ConsensusReadForwardHandler();
private static final Logger logger = LoggerFactory.getLogger(ConsensusReadForwardHandler.class);
@Override
public void doVerb(Message<ConsensusReadForwardRequest> message)
{
ConsensusReadForwardRequest request = message.payload;
SinglePartitionReadCommand command = request.command;
Tracing.trace("Executing forwarded consensus read operation for {}", command.partitionKey());
// Start capturing client warnings for the forwarded operation
ClientWarn.instance.captureWarnings();
try
{
// Validate keyspace exists and is tracked
String keyspaceName = command.metadata().keyspace;
KeyspaceMetadata ksMetadata = Schema.instance.getKeyspaceMetadata(keyspaceName);
if (ksMetadata == null)
{
MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message);
logger.error("Failed to forward consensus read operation for non-existent keyspace {}", keyspaceName);
return;
}
if (!ksMetadata.params.replicationType.isTracked())
{
MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message);
logger.error("Asked to perform forwarded consensus read operation, but keyspace {} is not tracked", keyspaceName);
return;
}
// Create a Group from the single command for reading
SinglePartitionReadCommand.Group group = SinglePartitionReadCommand.Group.one(command);
// Execute the read using StorageProxy.read() which will:
// 1. Check forwarding (returns null since we're on a replica)
// 2. Execute the consensus read with the appropriate protocol
logger.debug("Executing consensus read operation for table {}.{} with key {}",
keyspaceName, command.metadata().name, command.partitionKey());
Dispatcher.RequestTime requestTime = Dispatcher.RequestTime.forImmediateExecution();
PartitionIterator result = StorageProxy.readWithConsensusForwarded(group, request.consistencyLevel, requestTime);
// Create response with the read result and captured warnings
List<String> warnings = ClientWarn.instance.getWarnings();
CasForwardResponse response = new CasForwardResponse(result, warnings);
MessagingService.instance().respond(response, message);
logger.debug("Completed forwarded consensus read operation for {}", command.partitionKey());
}
catch (CassandraException ce)
{
// Forward the exception back to the original coordinator with warnings
List<String> warnings = ClientWarn.instance.getWarnings();
CasForwardResponse response = new CasForwardResponse(ce, warnings);
MessagingService.instance().respond(response, message);
}
catch (Throwable t)
{
try
{
MessagingService.instance().respondWithFailure(RequestFailure.forException(t), message);
}
finally
{
throw t;
}
}
finally
{
ClientWarn.instance.resetWarnings();
}
}
}

View File

@ -0,0 +1,77 @@
/*
* 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.service.paxos;
import java.io.IOException;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.SinglePartitionReadCommand;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
/**
* Request to forward a consensus read operation to a replica coordinator.
* This is used when the original coordinator is not a replica but needs to
* execute a consensus read for a tracked keyspace that requires proper coordination.
*
* Consensus reads only ever contain a single read command.
*/
public class ConsensusReadForwardRequest
{
public static final Serializer serializer = new Serializer();
public final SinglePartitionReadCommand command;
public final ConsistencyLevel consistencyLevel;
public ConsensusReadForwardRequest(SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel)
{
this.command = command;
this.consistencyLevel = consistencyLevel;
}
public static class Serializer implements IVersionedSerializer<ConsensusReadForwardRequest>
{
@Override
public void serialize(ConsensusReadForwardRequest forwardRequest, DataOutputPlus out, int version) throws IOException
{
ReadCommand.serializer.serialize(forwardRequest.command, out, version);
out.writeByte(forwardRequest.consistencyLevel.code);
}
@Override
public ConsensusReadForwardRequest deserialize(DataInputPlus in, int version) throws IOException
{
ReadCommand readCommand = ReadCommand.serializer.deserialize(in, version);
if (!(readCommand instanceof SinglePartitionReadCommand))
throw new IOException("Expected SinglePartitionReadCommand but got " + readCommand.getClass());
ConsistencyLevel consistencyLevel = ConsistencyLevel.fromCode(in.readUnsignedByte());
return new ConsensusReadForwardRequest((SinglePartitionReadCommand) readCommand, consistencyLevel);
}
@Override
public long serializedSize(ConsensusReadForwardRequest forwardRequest, int version)
{
long size = ReadCommand.serializer.serializedSize(forwardRequest.command, version);
size += 1; // consistencyLevel.code
return size;
}
}
}

View File

@ -21,6 +21,7 @@ package org.apache.cassandra.service.paxos;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@ -41,9 +42,13 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.statements.CQL3CasRequest;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.IReadResponse;
import org.apache.cassandra.db.ReadKind;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.ReadResponse;
import org.apache.cassandra.db.SinglePartitionReadCommand;
import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.db.partitions.FilteredPartition;
@ -53,7 +58,6 @@ import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.CasWriteTimeoutException;
import org.apache.cassandra.exceptions.ExceptionCode;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.IsBootstrappingException;
import org.apache.cassandra.exceptions.ReadFailureException;
@ -83,10 +87,10 @@ import org.apache.cassandra.locator.ReplicaLayout.ForTokenWrite;
import org.apache.cassandra.locator.ReplicaPlan.ForRead;
import org.apache.cassandra.metrics.ClientRequestMetrics;
import org.apache.cassandra.metrics.ClientRequestSizeMetrics;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.CASRequest;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.FailureRecordingCallback.AsMap;
import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter;
@ -98,6 +102,7 @@ import org.apache.cassandra.service.paxos.cleanup.PaxosRepairState;
import org.apache.cassandra.service.reads.DataResolver;
import org.apache.cassandra.service.reads.ReadCoordinator;
import org.apache.cassandra.service.reads.repair.NoopReadRepair;
import org.apache.cassandra.service.reads.tracked.TrackedDataResponse;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tcm.membership.NodeId;
@ -109,6 +114,8 @@ import org.apache.cassandra.utils.CassandraVersion;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.NoSpamLogger;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Collections.emptyMap;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
@ -353,7 +360,7 @@ public class Paxos
/**
* Encapsulates the peers we will talk to for this operation.
*/
static class Participants implements ForRead<EndpointsForToken, Participants>
public static class Participants implements ForRead<EndpointsForToken, Participants>
{
final Keyspace keyspace;
@ -499,6 +506,11 @@ public class Paxos
return electorateLive.endpoint(i);
}
Replica voterReplica(int i)
{
return electorateLive.get(i);
}
void assureSufficientLiveNodes(boolean isWrite) throws UnavailableException
{
if (sizeOfConsensusQuorum > sizeOfPoll())
@ -635,7 +647,9 @@ public class Paxos
if (isFailure)
{
mark(isWrite, m -> m.failures, consistency);
throw serverError != null ? new RequestFailureException(ExceptionCode.SERVER_ERROR, serverError, consistency, successes, required, failures)
throw serverError != null ? (isWrite
? new WriteFailureException(consistency, successes, required, WriteType.CAS, failures)
: new ReadFailureException(serverError, consistency, successes, required, false, failures, null))
: isWrite
? new WriteFailureException(consistency, successes, required, WriteType.CAS, failures)
: new ReadFailureException(consistency, successes, required, false, failures);
@ -706,7 +720,7 @@ public class Paxos
* (since, if the CAS doesn't succeed, it means the current value do not match the conditions).
*/
public static ConsensusAttemptResult cas(DecoratedKey partitionKey,
CASRequest request,
CQL3CasRequest request,
ConsistencyLevel consistencyForConsensus,
ConsistencyLevel consistencyForCommit,
ClientState clientState,
@ -752,6 +766,7 @@ public class Paxos
Proposal proposal;
boolean conditionMet = request.appliesTo(current);
if (!conditionMet)
{
if (getPaxosVariant() == v2_without_linearizable_reads_or_rejected_writes)
@ -823,7 +838,7 @@ public class Paxos
// no need to commit a no-op; either it
// 1) reached a majority, in which case it was agreed, had no effect and we can do nothing; or
// 2) did not reach a majority, was not agreed, and was not user visible as a result so we can ignore it
if (!proposal.update.isEmpty())
if (!proposal.isEmpty())
commit = commit(proposal.agreed(), participants, consistencyForConsensus, consistencyForCommit, true);
break done;
@ -1125,15 +1140,43 @@ public class Paxos
PaxosPrepare.Success success = prepare.success();
Supplier<Participants> plan = () -> success.participants;
DataResolver<?, ?> resolver = new DataResolver<>(ReadCoordinator.DEFAULT, query, plan, NoopReadRepair.instance, requestTime);
for (int i = 0 ; i < success.responses.size() ; ++i)
resolver.preprocess(success.responses.get(i));
List<Message<IReadResponse>> responses = success.responses;
class WasRun implements Runnable { boolean v; public void run() { v = true; } }
WasRun hadShortRead = new WasRun();
PartitionIterator result = resolver.resolve(hadShortRead);
// There should be only a single response from the coordinator that was selected to do the tracked read
boolean isTracked = responses.get(0).payload.kind().isTracked();
if (!isPromised && hadShortRead.v)
PartitionIterator result = null;
boolean hadShortRead = false;
if (isTracked)
{
for (Message<IReadResponse> response : responses)
{
if (response.payload.kind() == ReadKind.TRACKED_DATA)
{
result = ((TrackedDataResponse) response.payload).makeIterator(query);
break;
}
}
checkState(result != null, "Should have found a data response");
}
else
{
DataResolver<EndpointsForToken, Participants> resolver = new DataResolver<>(ReadCoordinator.DEFAULT, query, plan, NoopReadRepair.instance, requestTime);
for (int i = 0 ; i < responses.size() ; ++i)
{
Message<IReadResponse> message = responses.get(i);
resolver.preprocess(message.withPayload((ReadResponse)message.payload));
}
// SERIAL supports partition range reads which can result in short reads
class WasRun implements Runnable { boolean v; public void run() { v = true; } }
WasRun hadShortReadRunnable = new WasRun();
result = resolver.resolve(hadShortReadRunnable);
hadShortRead = hadShortReadRunnable.v;
}
if (!isPromised && hadShortRead)
{
// we need to propose an empty update to linearize our short read, but only had read success
// since we may continue to perform short reads, we ask our prepare not to accept an early
@ -1143,7 +1186,7 @@ public class Paxos
break;
}
return new BeginResult(success.ballot, success.participants, failedAttemptsDueToContention, result, !hadShortRead.v && success.isReadSafe, isPromised, success.supersededBy, false);
return new BeginResult(success.ballot, success.participants, failedAttemptsDueToContention, result, !hadShortRead && success.isReadSafe, isPromised, success.supersededBy, false);
}
case MAYBE_FAILURE:
@ -1170,7 +1213,7 @@ public class Paxos
}
}
public static boolean isInRangeAndShouldProcess(InetAddressAndPort from, DecoratedKey key, TableMetadata table, boolean includesRead)
public static boolean isInRangeAndShouldProcess(DecoratedKey key, TableMetadata table, boolean includesRead)
{
Keyspace keyspace = Keyspace.open(table.keyspace);
// MetaStrategy distributes the entire keyspace to all replicas. In addition, its tables (currently only
@ -1324,7 +1367,7 @@ public class Paxos
public static void setPaxosVariant(Config.PaxosVariant paxosVariant)
{
Preconditions.checkNotNull(paxosVariant);
checkNotNull(paxosVariant);
PAXOS_VARIANT = paxosVariant;
DatabaseDescriptor.setPaxosVariant(paxosVariant);
}

View File

@ -0,0 +1,132 @@
/*
* 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.service.paxos;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.NoPayload;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.concurrent.ConditionAsConsumer;
import static org.apache.cassandra.utils.concurrent.ConditionAsConsumer.newConditionAsConsumer;
/**
* Handler for forwarded Paxos V2 commit requests.
* Executes the commit operation on behalf of the original coordinator,
* ensuring that MutationId generation happens on a replica coordinator.
*
* The PaxosCommit constructor handles mutation ID generation, so this handler
* simply delegates to PaxosCommit.commit() with the original commit.
*
* TODO (expected): more comprehensive testing
*/
public class Paxos2CommitForwardHandler implements IVerbHandler<Paxos2CommitForwardRequest>
{
public static final Paxos2CommitForwardHandler instance = new Paxos2CommitForwardHandler();
private static final Logger logger = LoggerFactory.getLogger(Paxos2CommitForwardHandler.class);
@Override
public void doVerb(Message<Paxos2CommitForwardRequest> message)
{
// Ensure we have up-to-date cluster metadata before executing the forwarded commit
ClusterMetadataService.instance().fetchLogFromPeerOrCMS(message.from(), message.header.epoch);
Paxos2CommitForwardRequest request = message.payload;
Tracing.trace("Executing forwarded Paxos V2 commit for {}", request.commit.partitionKey());
try
{
String ksName = request.commit.metadata().keyspace;
KeyspaceMetadata ksMetadata = Schema.instance.getKeyspaceMetadata(ksName);
if (ksMetadata == null)
{
MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message);
logger.error("Failed to forward paxos commit for non-existent keyspace {}", ksName);
return;
}
if (!ksMetadata.params.replicationType.isTracked())
{
MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message);
logger.error("Asked to perform forwarded paxos commit, but keyspace {} is not tracked", ksName);
return;
}
// Execute the commit operation - PaxosCommit constructor handles mutation ID generation
ConditionAsConsumer<PaxosCommit.Status> onDone = newConditionAsConsumer();
PaxosCommit.Status[] statusHolder = new PaxosCommit.Status[1];
Consumer<PaxosCommit.Status> statusCapture = status -> {
statusHolder[0] = status;
onDone.accept(status);
};
// Delegate to PaxosCommit.commit() - it will generate mutation ID in constructor
PaxosCommit.commit(request.commit,
request.all,
request.allLive,
request.allDown,
request.required,
request.isUrgent,
request.consistencyForConsensus,
request.consistencyForCommit,
false, // allowHints
statusCapture);
// Wait for completion
try
{
onDone.awaitUntil(message.expiresAtNanos());
PaxosCommit.Status status = statusHolder[0];
if (status != null && status.isSuccess())
{
MessagingService.instance().respond(NoPayload.noPayload, message);
}
else
{
MessagingService.instance().respondWithFailure(RequestFailureReason.UNKNOWN, message);
logger.error("Forwarded Paxos V2 commit failed with status: {}", status);
}
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
MessagingService.instance().respondWithFailure(RequestFailure.forException(e), message);
logger.error("Forwarded Paxos V2 commit interrupted", e);
}
}
catch (Exception e)
{
MessagingService.instance().respondWithFailure(RequestFailure.forException(e), message);
logger.error("Failed to execute forwarded Paxos V2 commit for {}", request.commit, e);
}
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.service.paxos;
import java.io.IOException;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.EndpointsForToken;
import org.apache.cassandra.service.paxos.Commit.Agreed;
/**
* Request to forward a Paxos V2 commit operation to a replica coordinator.
* This is used when the original coordinator is not a replica but needs to
* execute a Paxos commit for a tracked keyspace that requires MutationId generation.
*
* Contains only the essential data needed by PaxosCommit instead of the full Participants object.
*/
public class Paxos2CommitForwardRequest
{
public static final Serializer serializer = new Serializer();
public final Agreed commit;
public final ConsistencyLevel consistencyForConsensus;
public final ConsistencyLevel consistencyForCommit;
public final EndpointsForToken all;
public final EndpointsForToken allLive;
public final EndpointsForToken allDown;
public final int required;
public final boolean isUrgent;
public Paxos2CommitForwardRequest(Agreed commit,
ConsistencyLevel consistencyForConsensus,
ConsistencyLevel consistencyForCommit,
EndpointsForToken all,
EndpointsForToken allLive,
EndpointsForToken allDown,
int required,
boolean isUrgent)
{
this.commit = commit;
this.consistencyForConsensus = consistencyForConsensus;
this.consistencyForCommit = consistencyForCommit;
this.all = all;
this.allLive = allLive;
this.allDown = allDown;
this.required = required;
this.isUrgent = isUrgent;
}
public static class Serializer implements IVersionedSerializer<Paxos2CommitForwardRequest>
{
@Override
public void serialize(Paxos2CommitForwardRequest request, DataOutputPlus out, int version) throws IOException
{
Agreed.serializer.serialize(request.commit, out, version);
out.writeByte(request.consistencyForConsensus.code);
out.writeByte(request.consistencyForCommit.code);
EndpointsForToken.serializer.serialize(request.all, out, version);
EndpointsForToken.serializer.serialize(request.allLive, out, version);
EndpointsForToken.serializer.serialize(request.allDown, out, version);
out.writeUnsignedVInt32(request.required);
out.writeBoolean(request.isUrgent);
}
@Override
public Paxos2CommitForwardRequest deserialize(DataInputPlus in, int version) throws IOException
{
Agreed commit = Agreed.serializer.deserialize(in, version);
ConsistencyLevel consistencyForConsensus = ConsistencyLevel.fromCode(in.readUnsignedByte());
ConsistencyLevel consistencyForCommit = ConsistencyLevel.fromCode(in.readUnsignedByte());
IPartitioner partitioner = commit.metadata().partitioner;
EndpointsForToken all = EndpointsForToken.serializer.deserialize(in, partitioner, version);
EndpointsForToken allLive = EndpointsForToken.serializer.deserialize(in, partitioner, version);
EndpointsForToken allDown = EndpointsForToken.serializer.deserialize(in, partitioner, version);
int required = in.readUnsignedVInt32();
boolean isUrgent = in.readBoolean();
return new Paxos2CommitForwardRequest(commit, consistencyForConsensus, consistencyForCommit,
all, allLive, allDown, required, isUrgent);
}
@Override
public long serializedSize(Paxos2CommitForwardRequest request, int version)
{
long size = Agreed.serializer.serializedSize(request.commit, version)
+ 1 // consistencyForConsensus.code
+ 1; // consistencyForCommit.code
size += EndpointsForToken.serializer.serializedSize(request.all, version);
size += EndpointsForToken.serializer.serializedSize(request.allLive, version);
size += EndpointsForToken.serializer.serializedSize(request.allDown, version);
size += TypeSizes.sizeofUnsignedVInt(request.required);
size += TypeSizes.BOOL_SIZE; // isUrgent
return size;
}
}
}

View File

@ -21,14 +21,18 @@ package org.apache.cassandra.service.paxos;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.agrona.collections.IntHashSet;
import org.apache.cassandra.concurrent.ExecutorPlus;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.locator.EndpointsForToken;
import org.apache.cassandra.locator.InOurDc;
@ -39,10 +43,17 @@ import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.NoPayload;
import org.apache.cassandra.net.RequestCallback;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.service.paxos.Paxos.Participants;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.concurrent.ConditionAsConsumer;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Collections.emptyMap;
import static org.apache.cassandra.exceptions.RequestFailureReason.UNKNOWN;
import static org.apache.cassandra.net.Verb.PAXOS2_COMMIT_REMOTE_REQ;
@ -102,6 +113,9 @@ public class PaxosCommit<OnDone extends Consumer<? super PaxosCommit.Status>> ex
final int required;
final OnDone onDone;
@Nullable
final IntHashSet remoteReplicas;
/**
* packs two 32-bit integers;
* bit 00-31: accepts
@ -112,15 +126,52 @@ public class PaxosCommit<OnDone extends Consumer<? super PaxosCommit.Status>> ex
*/
private volatile long responses;
public PaxosCommit(Agreed commit, boolean allowHints, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, Participants participants, OnDone onDone)
public PaxosCommit(Agreed commit, boolean allowHints, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, EndpointsForToken replicas, int required, OnDone onDone)
{
this.commit = commit;
// Check if this is a tracked keyspace
boolean isTracked = commit.metadata().replicationType().isTracked();
Agreed commitToUse = commit;
IntHashSet remoteReplicas = null;
if (isTracked)
{
// Precondition: for tracked keyspaces, the local node must be a replica
// so it can generate an ID
InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort();
checkState(replicas.endpoints().contains(localEndpoint),
"For tracked keyspaces, the coordinator must be a replica. Local endpoint %s not in replicas %s",
localEndpoint, replicas.endpoints());
// Generate mutation ID if the commit doesn't already have one
// (commits loaded from system.paxos may already have the saved mutation ID)
if (commit.mutation.id().isNone())
{
Token token = commit.partitionKey().getToken();
MutationId mutationId = MutationTrackingService.instance.nextMutationId(commit.metadata().keyspace, token);
Mutation mutationWithId = commit.makeMutation(mutationId);
commitToUse = new Commit.Agreed(commit.ballot, mutationWithId);
}
// Collect remote replicas for tracking service
remoteReplicas = new IntHashSet();
ClusterMetadata metadata = ClusterMetadata.current();
for (int i = 0; i < replicas.size(); i++)
{
Replica replica = replicas.get(i);
if (!replica.isSelf())
remoteReplicas.add(metadata.directory.peerId(replica.endpoint()).id());
}
}
this.commit = commitToUse;
this.allowHints = allowHints;
this.consistencyForConsensus = consistencyForConsensus;
this.consistencyForCommit = consistencyForCommit;
this.replicas = participants.all;
this.replicas = replicas;
this.onDone = onDone;
this.required = participants.requiredFor(consistencyForCommit);
this.required = required;
this.remoteReplicas = remoteReplicas;
if (required == 0)
onDone.accept(status());
}
@ -128,14 +179,45 @@ public class PaxosCommit<OnDone extends Consumer<? super PaxosCommit.Status>> ex
/**
* Submit the proposal for commit with all replicas, and wait synchronously until at most {@code deadline} for the result
*/
static Paxos.Async<Status> commit(Agreed commit, Participants participants, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, /** @deprecated See CASSANDRA-17164 */ @Deprecated(since = "4.1") boolean allowHints)
static Paxos.Async<Status> commit(Agreed commit, EndpointsForToken all, EndpointsForToken allLive, EndpointsForToken allDown, int required, boolean isUrgent, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, /** @deprecated See CASSANDRA-17164 */ @Deprecated(since = "4.1") boolean allowHints)
{
// Check if this is a tracked keyspace requiring forwarding to a replica coordinator
if (isTrackedKeyspaceRequiringForwarding(commit, all))
{
// For async version, create a wrapper that handles forwarding
Status[] statusHolder = new Status[1];
ConditionAsConsumer<Status> condition = newConditionAsConsumer();
Consumer<Status> statusCapture = status -> {
statusHolder[0] = status;
condition.accept(status);
};
forwardPaxos2Commit(commit, all, allLive, allDown, required, isUrgent, consistencyForConsensus, consistencyForCommit, statusCapture);
return new Paxos.Async<Status>()
{
@Override
public Status awaitUntil(long deadline)
{
try
{
condition.awaitUntil(deadline);
return statusHolder[0] != null ? statusHolder[0] : new Status(new Paxos.MaybeFailure(true, all.size(), required, 0, emptyMap()));
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
return new Status(new Paxos.MaybeFailure(true, all.size(), required, 0, emptyMap()));
}
}
};
}
// to avoid unnecessary object allocations we extend PaxosPropose to implements Paxos.Async
class Async extends PaxosCommit<ConditionAsConsumer<Status>> implements Paxos.Async<Status>
{
private Async(Agreed commit, boolean allowHints, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, Participants participants)
private Async(Agreed commit, boolean allowHints, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, EndpointsForToken all, int required)
{
super(commit, allowHints, consistencyForConsensus, consistencyForCommit, participants, newConditionAsConsumer());
super(commit, allowHints, consistencyForConsensus, consistencyForCommit, all, required, newConditionAsConsumer());
}
public Status awaitUntil(long deadline)
@ -154,38 +236,126 @@ public class PaxosCommit<OnDone extends Consumer<? super PaxosCommit.Status>> ex
}
}
Async async = new Async(commit, allowHints, consistencyForConsensus, consistencyForCommit, participants);
async.start(participants, false);
Async async = new Async(commit, allowHints, consistencyForConsensus, consistencyForCommit, all, required);
async.start(allLive, allDown, isUrgent, false);
return async;
}
/**
* Submit the proposal for commit with all replicas, and wait synchronously until at most {@code deadline} for the result
*/
static <T extends Consumer<Status>> T commit(Agreed commit, EndpointsForToken all, EndpointsForToken allLive, EndpointsForToken allDown, int required, boolean isUrgent, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, /** @deprecated See CASSANDRA-17164 */ @Deprecated(since = "4.1") boolean allowHints, T onDone)
{
// Check if this is a tracked keyspace requiring forwarding to a replica coordinator
if (isTrackedKeyspaceRequiringForwarding(commit, all))
{
forwardPaxos2Commit(commit, all, allLive, allDown, required, isUrgent, consistencyForConsensus, consistencyForCommit, onDone);
return onDone;
}
new PaxosCommit<>(commit, allowHints, consistencyForConsensus, consistencyForCommit, all, required, onDone)
.start(allLive, allDown, isUrgent, true);
return onDone;
}
static Paxos.Async<Status> commit(Agreed commit, Participants participants, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, /** @deprecated See CASSANDRA-17164 */ @Deprecated(since = "4.1") boolean allowHints)
{
return commit(commit, participants.all, participants.allLive, participants.allDown,
participants.requiredFor(consistencyForCommit), participants.isUrgent(),
consistencyForConsensus, consistencyForCommit, allowHints);
}
static <T extends Consumer<Status>> T commit(Agreed commit, Participants participants, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, /** @deprecated See CASSANDRA-17164 */ @Deprecated(since = "4.1") boolean allowHints, T onDone)
{
new PaxosCommit<>(commit, allowHints, consistencyForConsensus, consistencyForCommit, participants, onDone)
.start(participants, true);
return onDone;
return commit(commit, participants.all, participants.allLive, participants.allDown,
participants.requiredFor(consistencyForCommit), participants.isUrgent(),
consistencyForConsensus, consistencyForCommit, allowHints, onDone);
}
/**
* Send commit messages to peers (or self)
*/
void start(Participants participants, boolean async)
void start(EndpointsForToken allLive, EndpointsForToken allDown, boolean isUrgent, boolean async)
{
boolean executeOnSelf = false;
Message<Agreed> commitMessage = Message.out(PAXOS_COMMIT_REQ, commit, participants.isUrgent());
Message<Agreed> commitMessage = Message.out(PAXOS_COMMIT_REQ, commit, isUrgent);
Message<Mutation> mutationMessage = null;
if (ENABLE_DC_LOCAL_COMMIT && consistencyForConsensus.isDatacenterLocal())
mutationMessage = Message.out(PAXOS2_COMMIT_REMOTE_REQ, commit.makeMutation(), participants.isUrgent());
mutationMessage = Message.out(PAXOS2_COMMIT_REMOTE_REQ, commit.makeMutation(), isUrgent);
for (int i = 0, mi = participants.allLive.size(); i < mi ; ++i)
executeOnSelf |= isSelfOrSend(commitMessage, mutationMessage, participants.allLive.endpoint(i));
// For tracked keyspaces, the local commit MUST execute synchronously BEFORE sending to remote replicas.
// This ensures the mutation is written to the journal before any failure callback can trigger
// reconciliation via ActiveLogReconciler. Without this ordering, a fast remote failure could schedule
// reconciliation for a mutation that hasn't been journaled yet, causing NullPointerException.
boolean isTrackedKeyspace = remoteReplicas != null;
boolean localExecutedSynchronously = false;
InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort();
for (int i = 0, mi = participants.allDown.size(); i < mi ; ++i)
onFailure(participants.allDown.endpoint(i), RequestFailure.NODE_DOWN);
if (isTrackedKeyspace)
{
// For tracked keyspaces, we MUST execute locally synchronously, regardless of USE_SELF_EXECUTION setting.
// This is critical because retries are scheduled on the local ActiveLogReconciler and look up mutations
// in the local MutationJournal. If we don't execute synchronously first, a fast remote failure could
// trigger a retry before the mutation that hasn't been journaled yet, causing NullPointerException.
//
// We check BOTH allLive AND allDown because the local endpoint might be incorrectly considered DOWN
// (e.g., in simulation or during network partition recovery). If local is in allDown, we STILL need
// to execute locally to write to the journal, but we'll also skip calling onFailure for self below.
boolean localIsReplica = false;
for (int i = 0, mi = allLive.size(); i < mi; ++i)
{
if (allLive.endpoint(i).equals(localEndpoint))
{
localIsReplica = true;
break;
}
}
if (!localIsReplica)
{
for (int i = 0, mi = allDown.size(); i < mi; ++i)
{
if (allDown.endpoint(i).equals(localEndpoint))
{
localIsReplica = true;
break;
}
}
}
if (localIsReplica)
{
executeOnSelf();
localExecutedSynchronously = true;
}
}
// Now send to remote replicas (and record local execution for non-tracked keyspaces)
boolean executeOnSelf = false;
for (int i = 0, mi = allLive.size(); i < mi ; ++i)
{
InetAddressAndPort endpoint = allLive.endpoint(i);
// Skip self if we already executed synchronously for tracked keyspace.
// Use direct comparison instead of shouldExecuteOnSelf to avoid dependence on USE_SELF_EXECUTION.
if (localExecutedSynchronously && endpoint.equals(localEndpoint))
continue;
executeOnSelf |= isSelfOrSend(commitMessage, mutationMessage, endpoint);
}
for (int i = 0, mi = allDown.size(); i < mi ; ++i)
{
InetAddressAndPort endpoint = allDown.endpoint(i);
// Skip self if we already executed synchronously for tracked keyspace.
// We can't "retry" to self via network anyway, and we've already written to the journal.
if (localExecutedSynchronously && endpoint.equals(localEndpoint))
continue;
onFailure(endpoint, RequestFailure.NODE_DOWN);
}
// Tracked if remoteReplicas != null, register write request with tracking service for tracked keyspaces
if (remoteReplicas != null)
{
checkState(!remoteReplicas.isEmpty());
MutationTrackingService.instance.sentWriteRequest(commit.makeMutation(), remoteReplicas);
}
if (executeOnSelf)
{
@ -218,6 +388,11 @@ public class PaxosCommit<OnDone extends Consumer<? super PaxosCommit.Status>> ex
return locator.local().sameDatacenter(locator.location(destination));
}
private boolean isTracked()
{
return !commit.mutation.id().equals(MutationId.none());
}
/**
* Record a failure or timeout, and maybe submit a hint to {@code from}
*/
@ -227,6 +402,10 @@ public class PaxosCommit<OnDone extends Consumer<? super PaxosCommit.Status>> ex
if (logger.isTraceEnabled())
logger.trace("{} {} from {}", commit, reason, from);
// Track failed response for tracked keyspaces
if (isTracked())
MutationTrackingService.instance.retryFailedWrite(commit.mutation.id(), from, reason);
response(false, from);
Replica replica = replicas.lookup(from);
@ -241,6 +420,11 @@ public class PaxosCommit<OnDone extends Consumer<? super PaxosCommit.Status>> ex
{
logger.trace("{} Success from {}", commit, response.from());
// Track successful response for tracked keyspaces
// (Local mutations are witnessed from Keyspace.applyInternalTracked)
if (isTracked())
MutationTrackingService.instance.receivedWriteResponse(commit.mutation.id(), response.from());
response(true, response.from());
}
@ -249,12 +433,40 @@ public class PaxosCommit<OnDone extends Consumer<? super PaxosCommit.Status>> ex
*/
public void executeOnSelf()
{
executeOnSelf(commit, RequestHandler::execute);
if (isTracked())
{
// For tracked keyspaces, local execution MUST succeed and write to journal.
// Use direct execution instead of executeOnSelf to ensure we detect failures.
NoPayload response = RequestHandler.execute(commit);
if (response == null)
{
throw new IllegalStateException(String.format(
"Local execution failed for tracked mutation %s. " +
"isInRangeAndShouldProcess returned false but this node is the coordinator for a tracked keyspace. " +
"partitionKey=%s, table=%s, localEndpoint=%s",
commit.mutation.id(), commit.partitionKey(), commit.metadata().keyspace + "." + commit.metadata().name,
FBUtilities.getBroadcastAddressAndPort()));
}
onResponse(response, FBUtilities.getBroadcastAddressAndPort());
}
else
{
executeOnSelf(commit, RequestHandler::execute);
}
}
@Override
public void onResponse(NoPayload response, InetAddressAndPort from)
{
// Track successful response for tracked keyspaces
if (isTracked())
{
if (response != null)
MutationTrackingService.instance.receivedWriteResponse(commit.mutation.id(), from);
else
MutationTrackingService.instance.retryFailedWrite(commit.mutation.id(), from, RequestFailure.UNKNOWN);
}
response(response != null, from);
}
@ -307,7 +519,7 @@ public class PaxosCommit<OnDone extends Consumer<? super PaxosCommit.Status>> ex
@Override
public void doVerb(Message<Agreed> message)
{
NoPayload response = execute(message.payload, message.from());
NoPayload response = execute(message.payload);
// NOTE: for correctness, this must be our last action, so that we cannot throw an error and send both a response and a failure response
if (response == null)
MessagingService.instance().respondWithFailure(UNKNOWN, message);
@ -315,15 +527,103 @@ public class PaxosCommit<OnDone extends Consumer<? super PaxosCommit.Status>> ex
MessagingService.instance().respond(response, message);
}
private static NoPayload execute(Agreed agreed, InetAddressAndPort from)
private static NoPayload execute(Agreed agreed)
{
if (!Paxos.isInRangeAndShouldProcess(from, agreed.update.partitionKey(), agreed.update.metadata(), false))
if (!Paxos.isInRangeAndShouldProcess(agreed.partitionKey(), agreed.metadata(), false))
return null;
PaxosState.commitDirect(agreed);
Tracing.trace("Enqueuing acknowledge to {}", from);
Tracing.trace("Enqueuing acknowledge to {}", agreed.ballot);
return NoPayload.noPayload;
}
}
/**
* Checks if this commit needs to be forwarded to a replica coordinator for tracked keyspace support.
*/
private static boolean isTrackedKeyspaceRequiringForwarding(Agreed commit, EndpointsForToken all)
{
if (!commit.metadata().replicationType().isTracked())
return false;
// Check if current coordinator is not a replica
InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort();
boolean isLocalReplica = all.endpoints().contains(localEndpoint);
return !isLocalReplica;
}
/**
* Forwards a Paxos V2 commit operation to a replica coordinator for tracked keyspaces.
*/
private static <T extends Consumer<Status>> void forwardPaxos2Commit(Agreed commit,
EndpointsForToken all,
EndpointsForToken allLive,
EndpointsForToken allDown,
int required,
boolean isUrgent,
ConsistencyLevel consistencyForConsensus,
ConsistencyLevel consistencyForCommit,
T onDone)
{
InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort();
// Filter out local endpoint and sort by proximity to find best replica to forward to
EndpointsForToken liveReplicasExcludingSelf = allLive.filter(r -> !r.endpoint().equals(localEndpoint));
if (liveReplicasExcludingSelf.isEmpty())
{
// No live replica available to forward to
logger.debug("No live replicas available to forward Paxos V2 commit for {}", commit.partitionKey());
Tracing.trace("No live replicas available to forward Paxos V2 commit");
onDone.accept(new Status(new Paxos.MaybeFailure(true, 1, 1, 0, emptyMap())));
return;
}
// Sort by proximity and select the best coordinator
EndpointsForToken sortedReplicas = DatabaseDescriptor.getNodeProximity().sortedByProximity(localEndpoint, liveReplicasExcludingSelf);
InetAddressAndPort replicaCoordinator = sortedReplicas.get(0).endpoint();
logger.debug("Forwarding Paxos V2 commit for {} to replica coordinator {}", commit.partitionKey(), replicaCoordinator);
Tracing.trace("Forwarding Paxos V2 commit to replica coordinator {}", replicaCoordinator);
// Create forward request with extracted participant data
Paxos2CommitForwardRequest forwardRequest = new Paxos2CommitForwardRequest(commit, consistencyForConsensus, consistencyForCommit,
all, allLive, allDown,
required, isUrgent);
Message<Paxos2CommitForwardRequest> message = Message.out(Verb.PAXOS2_COMMIT_FORWARD_REQ, forwardRequest);
// Create callback to handle forwarding response
RequestCallback<NoPayload> callback = new RequestCallback<NoPayload>()
{
@Override
public void onResponse(Message<NoPayload> response)
{
Tracing.trace("Forwarded Paxos V2 commit completed successfully");
onDone.accept(success);
}
@Override
public void onFailure(InetAddressAndPort from, RequestFailure failure)
{
logger.debug("Forwarded Paxos V2 commit to {} failed: {}", from, failure);
Tracing.trace("Forwarded Paxos V2 commit to {} failed: {}", from, failure);
// Populate the failure map with the actual failure reason; contacted=1, required=1 for forwarded request
onDone.accept(new Status(new Paxos.MaybeFailure(true, 1, 1, 0,
java.util.Collections.singletonMap(from, failure.reason))));
}
};
try
{
MessagingService.instance().sendWithCallback(message, replicaCoordinator, callback);
}
catch (Exception e)
{
logger.debug("Failed to send forwarded Paxos V2 commit to {}: {}", replicaCoordinator, e.getMessage());
Tracing.trace("Failed to send forwarded Paxos V2 commit: {}", e.getMessage());
onDone.accept(new Status(new Paxos.MaybeFailure(true, 1, 1, 0,
java.util.Collections.singletonMap(replicaCoordinator, UNKNOWN))));
}
}
}

View File

@ -20,11 +20,12 @@ package org.apache.cassandra.service.paxos;
import java.io.IOException;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.EmbeddableSinglePartitionReadCommand;
import org.apache.cassandra.db.SinglePartitionReadCommand;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
@ -34,8 +35,15 @@ import org.apache.cassandra.service.paxos.Commit.Agreed;
import org.apache.cassandra.service.paxos.PaxosPrepare.Rejected;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.service.paxos.PaxosPrepare.Response;
import org.apache.cassandra.service.reads.tracked.TrackedRead;
import org.apache.cassandra.service.reads.tracked.TrackedRead.Id;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher.RequestTime;
import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.ImmediateFuture;
import static com.google.common.util.concurrent.Futures.getUnchecked;
import static org.apache.cassandra.exceptions.RequestFailureReason.UNKNOWN;
import static org.apache.cassandra.net.Verb.PAXOS2_COMMIT_AND_PREPARE_REQ;
import static org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState.getKeyMigrationState;
@ -50,21 +58,44 @@ public class PaxosCommitAndPrepare
static PaxosPrepare commitAndPrepare(Agreed commit, Paxos.Participants participants, SinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadSuccess)
{
Ballot ballot = newBallot(commit.ballot, participants.consistencyForConsensus);
Request request = new Request(commit, ballot, participants.electorate, readCommand, isWrite, true);
PaxosPrepare prepare = new PaxosPrepare(participants, request, acceptEarlyReadSuccess, null);
Tracing.trace("Committing {}; Preparing {}", commit.ballot, ballot);
Message<Request> message = Message.out(PAXOS2_COMMIT_AND_PREPARE_REQ, request, participants.isUrgent());
/*
* For simplicity with tracked keyspaces do the commit as a regular commit synchronously and then separately do a regular prepare.
* CommitAndPrepare goes down the prepare path with a message containing the commit along with the prepare
* which means this node is the coordinator and would need to either re-use the original commit mutation id
* (which wasn't saved in the system table) or generate a new one which it might not be able to do without forwarding.
*
* All these things are tractable to do better, but for now doing something simple and correct.
*/
if (readCommand.metadata().replicationType().isTracked())
{
/*
* Consistency for consensus is tricky to pick here. The goal of sending this commit is to unblock the prepare
* on nodes that are missing the commit. CommitAndPrepare is an outcome that occurs when prepare/propose already failed
* because enough nodes were missing a commmit so we need to try again. To keep things highly available we
* use the same consistency as consensus so that when we go to do the prepare there are enough nodes
* we know have the commit that this can succeed.
*/
PaxosCommit.commit(commit, participants, participants.consistencyForConsensus, participants.consistencyForConsensus, isWrite);
return PaxosPrepare.prepareWithBallot(ballot, participants, readCommand, isWrite, acceptEarlyReadSuccess);
}
else
{
Request request = new Request(commit, ballot, participants.electorate, readCommand, isWrite, true);
PaxosPrepare prepare = new PaxosPrepare(participants, request, acceptEarlyReadSuccess, null);
Message<Request> message = Message.out(PAXOS2_COMMIT_AND_PREPARE_REQ, request, participants.isUrgent());
start(prepare, participants, message, RequestHandler::execute);
return prepare;
start(prepare, participants, message, RequestHandler::execute);
return prepare;
}
}
private static class Request extends PaxosPrepare.AbstractRequest<Request>
{
final Agreed commit;
Request(Agreed commit, Ballot ballot, Paxos.Electorate electorate, SinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery)
Request(Agreed commit, Ballot ballot, Paxos.Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery)
{
super(ballot, electorate, read, isWrite, isForRecovery);
this.commit = commit;
@ -81,6 +112,18 @@ public class PaxosCommitAndPrepare
return new Request(commit, ballot, electorate, partitionKey, table, isForWrite, isForRecovery);
}
@Override
public Request asTrackedDataRequest(Id id, ConsistencyLevel consistencyLevel, int dataNode, int[] summaryNodes)
{
return new Request(commit, ballot, electorate, new TrackedRead.DataRequest(id, (SinglePartitionReadCommand)read, dataNode, summaryNodes, consistencyLevel), isForWrite, isForRecovery);
}
@Override
public Request asTrackedSummaryRequest(Id id, int dataNode, int[] summaryNodes)
{
return new Request(commit, ballot, electorate, new TrackedRead.SummaryRequest(id, (SinglePartitionReadCommand)read, dataNode, summaryNodes), isForWrite, isForRecovery);
}
public String toString()
{
return commit.toString("CommitAndPrepare(") + ", " + Ballot.toString(ballot) + ')';
@ -89,7 +132,7 @@ public class PaxosCommitAndPrepare
public static class RequestSerializer extends PaxosPrepare.AbstractRequestSerializer<Request, Agreed>
{
Request construct(Agreed param, Ballot ballot, Paxos.Electorate electorate, SinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery)
Request construct(Agreed param, Ballot ballot, Paxos.Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery)
{
return new Request(param, ballot, electorate, read, isWrite, isForRecovery);
}
@ -128,32 +171,33 @@ public class PaxosCommitAndPrepare
{
ClusterMetadataService.instance().fetchLogFromPeerOrCMS(ClusterMetadata.current(), message.from(), message.epoch());
PaxosPrepare.Response response = execute(message.payload, message.from());
Future<Response> response = execute(message.payload, new RequestTime(message.createdAtNanos()));
if (response == null)
MessagingService.instance().respondWithFailure(UNKNOWN, message);
else
MessagingService.instance().respond(response, message);
// TODO unwrap error for error handling in the verb
MessagingService.instance().respond(getUnchecked(response), message);
}
private static PaxosPrepare.Response execute(Request request, InetAddressAndPort from)
private static Future<PaxosPrepare.Response> execute(Request request, RequestTime requestTime)
{
Agreed commit = request.commit;
if (!Paxos.isInRangeAndShouldProcess(from, commit.update.partitionKey(), commit.update.metadata(), request.read != null))
if (!Paxos.isInRangeAndShouldProcess(commit.partitionKey(), commit.metadata(), request.read != null))
return null;
// This can be done outside the lock
ClusterMetadata cm = ClusterMetadata.current();
KeyMigrationState keyMigrationState = getKeyMigrationState(cm, commit.update.metadata().id, commit.update.partitionKey());
KeyMigrationState keyMigrationState = getKeyMigrationState(cm, commit.metadata().id, commit.partitionKey());
// Make sure the operation is safe and there is no Accord state that needs application
// Also need to know max HLC in order to accept this ballot
long maxHLC = keyMigrationState.maybePerformAccordToPaxosKeyMigration(true);
if (maxHLC >= commit.ballot.unixMicros())
return new Rejected(Ballot.atUnixMicrosWithLsb(maxHLC + 1, 0, commit.ballot.flag()));
return ImmediateFuture.success(new Rejected(Ballot.atUnixMicrosWithLsb(maxHLC + 1, 0, commit.ballot.flag())));
try (PaxosState state = PaxosState.get(commit))
{
state.commit(commit);
return PaxosPrepare.RequestHandler.execute(request, state, cm);
return PaxosPrepare.RequestHandler.execute(requestTime, request, state, cm);
}
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.service.paxos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.NoPayload;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
/**
* Handler for forwarded Paxos V1 commit requests.
* Executes the commit operation on behalf of the original coordinator,
* ensuring that MutationId generation happens on a replica coordinator.
*
* TODO (expected): more comprehensive testing
*/
public class PaxosCommitForwardHandler implements IVerbHandler<PaxosCommitForwardRequest>
{
public static final PaxosCommitForwardHandler instance = new PaxosCommitForwardHandler();
private static final Logger logger = LoggerFactory.getLogger(PaxosCommitForwardHandler.class);
@Override
public void doVerb(Message<PaxosCommitForwardRequest> message)
{
// PaxosV1 when doing commit picks whatever the current replicas are to send the commits to
// so make sure we at least match what they would have picked
ClusterMetadataService.instance().fetchLogFromPeerOrCMS(message.from(), message.header.epoch);
PaxosCommitForwardRequest request = message.payload;
Tracing.trace("Executing forwarded Paxos commit for {}", request.proposal.partitionKey());
try
{
String ksName = request.proposal.metadata().keyspace;
Keyspace keyspace = Keyspace.openIfExists(ksName);
if (keyspace == null)
{
MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message);
logger.error("Failed to forward paxos commit for non-existent keyspace {}", ksName);
return;
}
if (!keyspace.getMetadata().params.replicationType.isTracked())
{
MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message);
logger.error("Asked to perform forwarded paxos commit, but keyspace {} is not tracked", ksName);
return;
}
// Call commitPaxosTracked which handles mutation ID generation, sending to all replicas,
// and tracking. The respondAfterSend flag determines if we wait for application.
StorageProxy.commitPaxosTracked(keyspace, request.proposal, request.consistencyLevel,
Dispatcher.RequestTime.forImmediateExecution(), request.respondAfterSend);
MessagingService.instance().respond(NoPayload.noPayload, message);
}
catch (Exception e)
{
MessagingService.instance().respondWithFailure(RequestFailure.forException(e), message);
logger.error("Failed to execute forwarded paxos commit for {}", request.proposal, e);
}
}
}

View File

@ -0,0 +1,88 @@
/*
* 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.service.paxos;
import java.io.IOException;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
/**
* Request to forward a Paxos V1 commit operation to a replica coordinator.
* This is used when the original coordinator is not a replica but needs to
* execute a Paxos commit for a tracked keyspace that requires MutationId generation.
*
* When {@code respondAfterSend} is true, the handler will respond immediately after
* sending the commits to replicas without waiting for application. This is used
* for the sendCommit path where we need to ensure commits were dispatched before
* continuing with a prepare operation.
*/
public class PaxosCommitForwardRequest
{
public static final Serializer serializer = new Serializer();
public final Commit proposal;
public final ConsistencyLevel consistencyLevel;
/**
* If true, the handler will respond immediately after sending the commits
* to replicas without waiting for application to complete.
*/
public final boolean respondAfterSend;
public PaxosCommitForwardRequest(Commit proposal, ConsistencyLevel consistencyLevel)
{
this(proposal, consistencyLevel, false);
}
public PaxosCommitForwardRequest(Commit proposal, ConsistencyLevel consistencyLevel, boolean respondAfterSend)
{
this.proposal = proposal;
this.consistencyLevel = consistencyLevel;
this.respondAfterSend = respondAfterSend;
}
public static class Serializer implements IVersionedSerializer<PaxosCommitForwardRequest>
{
@Override
public void serialize(PaxosCommitForwardRequest request, DataOutputPlus out, int version) throws IOException
{
Commit.serializer.serialize(request.proposal, out, version);
out.write(request.consistencyLevel.code);
out.writeBoolean(request.respondAfterSend);
}
@Override
public PaxosCommitForwardRequest deserialize(DataInputPlus in, int version) throws IOException
{
Commit proposal = Commit.serializer.deserialize(in, version);
ConsistencyLevel consistencyLevel = ConsistencyLevel.fromCode(in.readUnsignedByte());
boolean respondAfterSend = in.readBoolean();
return new PaxosCommitForwardRequest(proposal, consistencyLevel, respondAfterSend);
}
@Override
public long serializedSize(PaxosCommitForwardRequest request, int version)
{
return Commit.serializer.serializedSize(request.proposal, version)
+ 1 // consistencyLevel
+ 1; // respondAfterSend
}
}
}

View File

@ -36,9 +36,11 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.IReadResponse;
import org.apache.cassandra.db.EmbeddableSinglePartitionReadCommand;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.ReadResponse;
import org.apache.cassandra.db.SinglePartitionReadCommand;
@ -54,6 +56,12 @@ import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.service.reads.tracked.TrackedRead;
import org.apache.cassandra.service.reads.tracked.TrackedRead.DataRequest;
import org.apache.cassandra.service.reads.tracked.TrackedRead.Id;
import org.apache.cassandra.service.reads.tracked.TrackedRead.SummaryRequest;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.metrics.PaxosMetrics;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
@ -64,14 +72,20 @@ import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState.KeyMigrationState;
import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter;
import org.apache.cassandra.service.paxos.PaxosPrepare.Status.Outcome;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher.RequestTime;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.ImmediateFuture;
import org.apache.cassandra.utils.vint.VIntCoding;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.Futures.getUnchecked;
import static java.util.Collections.emptyMap;
import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
import static org.apache.cassandra.db.ReadKind.TRACKED_DATA;
import static org.apache.cassandra.exceptions.RequestFailureReason.UNKNOWN;
import static org.apache.cassandra.locator.InetAddressAndPort.Serializer.inetAddressAndPortSerializer;
import static org.apache.cassandra.net.Verb.PAXOS2_PREPARE_REQ;
@ -141,6 +155,8 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
private static Runnable onLinearizabilityViolation;
private static final Future<? extends IReadResponse> NO_READ_RESPONSE = ImmediateFuture.success(null);
public static final RequestHandler requestHandler = new RequestHandler();
public static final RequestSerializer requestSerializer = new RequestSerializer();
public static final ResponseSerializer responseSerializer = new ResponseSerializer();
@ -179,12 +195,12 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
static class Success extends WithRequestedBallot
{
final List<Message<ReadResponse>> responses;
final List<Message<IReadResponse>> responses;
final boolean isReadSafe; // read responses constitute a linearizable read (though short read protection would invalidate that)
final @Nullable
Ballot supersededBy; // if known and READ_SUCCESS
Success(Outcome outcome, Ballot ballot, Participants participants, List<Message<ReadResponse>> responses, boolean isReadSafe, @Nullable Ballot supersededBy)
Success(Outcome outcome, Ballot ballot, Participants participants, List<Message<IReadResponse>> responses, boolean isReadSafe, @Nullable Ballot supersededBy)
{
super(outcome, participants, ballot);
this.responses = responses;
@ -192,12 +208,12 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
this.supersededBy = supersededBy;
}
static Success read(Ballot ballot, Participants participants, List<Message<ReadResponse>> responses, @Nullable Ballot supersededBy)
static Success read(Ballot ballot, Participants participants, List<Message<IReadResponse>> responses, @Nullable Ballot supersededBy)
{
return new Success(Outcome.READ_PERMITTED, ballot, participants, responses, true, supersededBy);
}
static Success readOrWrite(Ballot ballot, Participants participants, List<Message<ReadResponse>> responses, boolean isReadConsistent)
static Success readOrWrite(Ballot ballot, Participants participants, List<Message<IReadResponse>> responses, boolean isReadConsistent)
{
return new Success(Outcome.PROMISED, ballot, participants, responses, isReadConsistent, null);
}
@ -327,8 +343,9 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
private final Participants participants;
private final List<Message<ReadResponse>> readResponses;
private final List<Message<IReadResponse>> readResponses;
private boolean haveReadResponseWithLatest;
private boolean haveTrackedDataResponseIfNeeded;
private boolean haveQuorumOfPermissions; // permissions => SUCCESS or READ_SUCCESS
private @Nonnull List<InetAddressAndPort> withLatest; // promised and have latest commit
private @Nullable List<InetAddressAndPort> needLatest; // promised without having witnessed latest commit, nor yet been refreshed by us
@ -361,7 +378,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
// no need to commit a no-op; either it
// 1) reached a majority, in which case it was agreed, had no effect and we can do nothing; or
// 2) did not reach a majority, was not agreed, and was not user visible as a result so we can ignore it
if (latestAccepted.update.isEmpty())
if (latestAccepted.isEmpty())
return false;
// If we aren't newer than latestCommitted, then we're done
@ -375,17 +392,17 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
return !latestAccepted.isReproposalOf(latestCommitted);
}
static PaxosPrepare prepare(Participants participants, SinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission) throws UnavailableException
static PaxosPrepare prepare(Participants participants, EmbeddableSinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission) throws UnavailableException
{
return prepare(null, participants, readCommand, isWrite, acceptEarlyReadPermission);
}
static PaxosPrepare prepare(Ballot minimumBallot, Participants participants, SinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission) throws UnavailableException
static PaxosPrepare prepare(Ballot minimumBallot, Participants participants, EmbeddableSinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission) throws UnavailableException
{
return prepareWithBallot(newBallot(minimumBallot, participants.consistencyForConsensus), participants, readCommand, isWrite, acceptEarlyReadPermission);
}
static PaxosPrepare prepareWithBallot(Ballot ballot, Participants participants, SinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission)
static PaxosPrepare prepareWithBallot(Ballot ballot, Participants participants, EmbeddableSinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission)
{
Tracing.trace("Preparing {} with read", ballot);
Request request = new Request(ballot, participants.electorate, readCommand, isWrite, false);
@ -411,22 +428,107 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
/**
* Submit the message to our peers, and submit it for local execution if relevant
*/
static <R extends AbstractRequest<R>> void start(PaxosPrepare prepare, Participants participants, Message<R> send, BiFunction<R, InetAddressAndPort, Response> selfHandler)
static <R extends AbstractRequest<R>> void start(PaxosPrepare prepare, Participants participants, Message<R> send, BiFunction<R, RequestTime, Future<Response>> selfHandler)
{
boolean executeOnSelf = false;
for (int i = 0, size = participants.sizeOfPoll() ; i < size ; ++i)
if (send.payload.read != null && send.payload.read.metadata().replicationType().isTracked())
startTracked(prepare, participants, send, selfHandler);
else
startUntracked(prepare, participants, send, selfHandler);
}
private static <R extends AbstractRequest<R>> void startTracked(PaxosPrepare prepare, Participants participants, Message<R> send, BiFunction<R, RequestTime, Future<Response>> selfHandler)
{
if (prepare.request.read == null)
prepare.haveTrackedDataResponseIfNeeded = true;
Message<R> selfMessage = null;
Message<R> summaryMessage = null;
Id readId = Id.nextId();
Replica localReplica = participants.lookup(FBUtilities.getBroadcastAddressAndPort());
Replica dataNode = localReplica != null && localReplica.isFull()
? localReplica
: null;
int[] summaryHostIds = new int[participants.sizeOfPoll() - 1]; // all nodes except data node
int summaryIndex = 0;
ClusterMetadata metadata = ClusterMetadata.current();
if (dataNode == null)
{
InetAddressAndPort destination = participants.voter(i);
boolean isPending = participants.electorate.isPending(destination);
logger.trace("{} to {}", send.payload, destination);
if (shouldExecuteOnSelf(destination))
executeOnSelf = true;
else
MessagingService.instance().sendWithCallback(isPending ? withoutRead(send) : send, destination, prepare);
for (int i = 0, size = participants.sizeOfPoll() ; i < size ; i++)
{
Replica replica = participants.voterReplica(i);
if (!replica.isFull() || participants.electorate.isPending(replica.endpoint()))
continue;
dataNode = replica;
break;
}
}
if (executeOnSelf)
send.verb().stage.execute(() -> prepare.executeOnSelf(send.payload, selfHandler));
checkState(dataNode != null, "Couldn't find a data node to use");
int dataNodeId = metadata.directory.peerId(dataNode.endpoint()).id();
for (int i = 0, size = participants.sizeOfPoll() ; i < size ; ++i)
{
Replica replica = participants.voterReplica(i);
if (replica != dataNode && !participants.electorate.isPending(replica.endpoint()))
summaryHostIds[summaryIndex++] = metadata.directory.peerId(replica.endpoint()).id();
}
for (int i = 0, size = participants.sizeOfPoll() ; i < size ; ++i)
{
Replica replica = participants.voterReplica(i);
InetAddressAndPort destination = replica.endpoint();
Message<R> toSendThisTime;
if (participants.electorate.isPending(destination))
toSendThisTime = withoutRead(send);
else if (replica == dataNode)
toSendThisTime = withTrackedDataRequest(send, readId, participants.consistencyLevel(), dataNodeId, summaryHostIds);
else
{
if (summaryMessage == null)
summaryMessage = withTrackedSummaryRequest(send, readId, dataNodeId, summaryHostIds);
toSendThisTime = summaryMessage;
}
logger.trace("{} to {}", toSendThisTime.payload, destination);
if (shouldExecuteOnSelf(destination))
selfMessage = toSendThisTime;
else
MessagingService.instance().sendWithCallback(toSendThisTime, destination, prepare);
}
if (selfMessage != null)
{
Message<R> selfMessageFinal = selfMessage;
send.verb().stage.execute(() -> prepare.executeOnSelfAsync(selfMessageFinal.payload, new RequestTime(selfMessageFinal.createdAtNanos()), selfHandler));
}
}
private static <R extends AbstractRequest<R>> void startUntracked(PaxosPrepare prepare, Participants participants, Message<R> send, BiFunction<R, RequestTime, Future<Response>> selfHandler)
{
prepare.haveTrackedDataResponseIfNeeded = true;
Message<R> selfMessage = null;
for (int i = 0, size = participants.sizeOfPoll() ; i < size ; ++i)
{
Replica replica = participants.voterReplica(i);
checkState(!replica.isTransient(), "Transient replication only supported with mutation tracking");
InetAddressAndPort destination = replica.endpoint();
Message<R> toSendThisTime = send;
if (participants.electorate.isPending(destination))
toSendThisTime = withoutRead(send);
logger.trace("{} to {}", toSendThisTime.payload, destination);
if (shouldExecuteOnSelf(destination))
selfMessage = toSendThisTime;
else
MessagingService.instance().sendWithCallback(toSendThisTime, destination, prepare);
}
if (selfMessage != null)
{
Message<R> selfMessageFinal = selfMessage;
send.verb().stage.execute(() -> prepare.executeOnSelfAsync(selfMessageFinal.payload, new RequestTime(selfMessageFinal.createdAtNanos()), selfHandler));
}
}
// TODO: extend Sync?
@ -450,6 +552,11 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
}
}
private boolean isTracked()
{
return request.read != null && request.read.metadata().replicationType().isTracked();
}
private boolean isDone()
{
return outcome != null;
@ -581,7 +688,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
}
}
if (!haveQuorumOfPermissions)
if (!haveQuorumOfPermissions || !haveTrackedDataResponseIfNeeded)
{
Committed newLatestCommitted = permitted.latestCommitted;
if (newLatestCommitted.ballot.uuidTimestamp() < maxLowBound) newLatestCommitted = Committed.none(request.partitionKey, request.table);
@ -656,9 +763,9 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
}
haveQuorumOfPermissions |= withLatest() + needLatest() >= participants.sizeOfConsensusQuorum;
if (haveQuorumOfPermissions)
if (haveQuorumOfPermissions && haveTrackedDataResponseIfNeeded)
{
if (request.read != null && readResponses.size() < participants.sizeOfReadQuorum)
if (request.read != null && !isTracked() && readResponses.size() < participants.sizeOfReadQuorum)
throw new IllegalStateException("Insufficient read responses: " + readResponses + "; need " + participants.sizeOfReadQuorum);
if (!hasOnlyPromises && !hasProposalStability)
@ -684,8 +791,10 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
{
refreshStaleParticipants();
// if an optimistic read is possible, and we are performing a read,
// we can safely answer immediately without waiting for the refresh
if (hasProposalStability && acceptEarlyReadPermission)
// we can safely answer immediately without waiting for the refresh.
// Check isDone() in case refreshStaleParticipants() completed synchronously
// and already signaled done via onRefreshSuccess() callback.
if (!isDone() && hasProposalStability && acceptEarlyReadPermission)
signalDone(Outcome.READ_PERMITTED);
}
@ -758,7 +867,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
// or in the case that we have an empty proposal accepted, since that will not be committed
// in theory in this case we could now restart refreshStaleParticipants, but this would
// unnecessarily complicate the logic so instead we accept that we will unnecessarily re-propose
if (latestAccepted != null && latestAccepted.update.isEmpty() && latestAccepted.isAfter(permitted.latestCommitted))
if (latestAccepted != null && latestAccepted.isEmpty() && latestAccepted.isAfter(permitted.latestCommitted))
return false;
// or in the case that both are older than the most recent repair low bound), in which case a topology change
@ -786,7 +895,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
}
}
long gcGraceMicros = TimeUnit.SECONDS.toMicros(permitted.latestCommitted.update.metadata().params.gcGraceSeconds);
long gcGraceMicros = TimeUnit.SECONDS.toMicros(permitted.latestCommitted.metadata().params.gcGraceSeconds);
// paxos repair uses stale ballots, so comparing against request.ballot time will not completely prevent false
// positives, since compaction may have removed paxos metadata on some nodes and not others. It's also possible
// clock skew has placed the ballot to repair in the future, so we use now or the ballot, whichever is higher.
@ -843,8 +952,10 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
*
* Must be invoked while owning lock
*/
private void addReadResponse(ReadResponse response, InetAddressAndPort from)
private void addReadResponse(IReadResponse response, InetAddressAndPort from)
{
if (response.kind() == TRACKED_DATA)
haveTrackedDataResponseIfNeeded = true;
readResponses.add(Message.synthetic(from, PAXOS2_PREPARE_RSP, response));
}
@ -961,13 +1072,13 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
{
final Ballot ballot;
final Electorate electorate;
final SinglePartitionReadCommand read;
final EmbeddableSinglePartitionReadCommand read;
final boolean isForWrite;
final DecoratedKey partitionKey;
final TableMetadata table;
final boolean isForRecovery;
AbstractRequest(Ballot ballot, Electorate electorate, SinglePartitionReadCommand read, boolean isForWrite, boolean isForRecovery)
AbstractRequest(Ballot ballot, Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isForWrite, boolean isForRecovery)
{
this.ballot = ballot;
this.electorate = electorate;
@ -991,6 +1102,10 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
abstract R withoutRead();
abstract R asTrackedDataRequest(Id id, ConsistencyLevel consistencyLevel, int dataNode, int[] summaryNodes);
abstract R asTrackedSummaryRequest(Id id, int dataNode, int[] summaryNodes);
public String toString()
{
return "Prepare(" + ballot + ')';
@ -999,7 +1114,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
static class Request extends AbstractRequest<Request>
{
Request(Ballot ballot, Electorate electorate, SinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery)
Request(Ballot ballot, Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery)
{
super(ballot, electorate, read, isWrite, isForRecovery);
}
@ -1018,6 +1133,18 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
{
return "Prepare(" + ballot + ')';
}
@Override
public Request asTrackedDataRequest(Id id, ConsistencyLevel consistencyLevel, int dataNode, int[] summaryNodes)
{
return new Request(ballot, electorate, new TrackedRead.DataRequest(id, (SinglePartitionReadCommand)read, dataNode, summaryNodes, consistencyLevel), isForWrite, isForRecovery);
}
@Override
public Request asTrackedSummaryRequest(Id id, int dataNode, int[] summaryNodes)
{
return new Request(ballot, electorate, new TrackedRead.SummaryRequest(id, (SinglePartitionReadCommand)read, dataNode, summaryNodes), isForWrite, isForRecovery);
}
}
static class Response
@ -1049,7 +1176,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
// a proposal that has been accepted but not committed, i.e. must be null or > latestCommit
@Nullable final Accepted latestAcceptedButNotCommitted;
final Committed latestCommitted;
@Nullable final ReadResponse readResponse;
@Nullable final IReadResponse readResponse;
// latestAcceptedButNotCommitted and latestCommitted were the same before and after the read occurred, and no incomplete promise was witnessed
final boolean hadProposalStability;
// it would be great if we could get rid of this, but probably we need to preserve for migration purposes
@ -1057,7 +1184,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
@Nullable final Ballot supersededBy;
final Epoch electorateEpoch;
Permitted(MaybePromise.Outcome outcome, long lowBound, @Nullable Accepted latestAcceptedButNotCommitted, Committed latestCommitted, @Nullable ReadResponse readResponse, boolean hadProposalStability, Map<InetAddressAndPort, EndpointState> gossipInfo, Epoch electorateEpoch, @Nullable Ballot supersededBy)
Permitted(MaybePromise.Outcome outcome, long lowBound, @Nullable Accepted latestAcceptedButNotCommitted, Committed latestCommitted, @Nullable IReadResponse readResponse, boolean hadProposalStability, Map<InetAddressAndPort, EndpointState> gossipInfo, Epoch electorateEpoch, @Nullable Ballot supersededBy)
{
super(outcome);
this.lowBound = lowBound;
@ -1103,11 +1230,26 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
try
{
Response response = execute(message.payload, message.from());
Future<Response> response = execute(message.payload, new RequestTime(message.createdAtNanos()));
if (response == null)
{
MessagingService.instance().respondWithFailure(UNKNOWN, message);
}
else if (response.isDone())
{
// TODO This will probably require exception unwrapping to get the correct error handling up to the message handler
// This also runs on the mutation stage and is waiting on distributed things which is sus
MessagingService.instance().respond(getUnchecked(response), message);
}
else
MessagingService.instance().respond(response, message);
{
response.addCallback((success, failure) -> {
if (failure != null)
MessagingService.instance().respondWithFailure(RequestFailure.forException(failure), message);
else
MessagingService.instance().respond(success, message);
});
}
}
catch (RetryOnDifferentSystemException e)
{
@ -1115,9 +1257,9 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
}
}
static Response execute(AbstractRequest<?> request, InetAddressAndPort from)
static Future<Response> execute(Request request, RequestTime requestTime)
{
if (!isInRangeAndShouldProcess(from, request.partitionKey, request.table, request.read != null))
if (!isInRangeAndShouldProcess(request.partitionKey, request.table, request.read != null))
return null;
long start = nanoTime();
@ -1135,12 +1277,13 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
// Also need to know max HLC in order to accept this ballot
long maxHLC = keyMigrationState.maybePerformAccordToPaxosKeyMigration(request.isForWrite);
if (maxHLC >= request.ballot.unixMicros())
return new Rejected(Ballot.atUnixMicrosWithLsb(maxHLC + 1, 0, request.ballot.flag()));
return ImmediateFuture.success(new Rejected(Ballot.atUnixMicrosWithLsb(maxHLC + 1, 0, request.ballot.flag())));
}
try (PaxosState state = get(request.partitionKey, request.table))
{
return execute(request, state, cm);
// TODO this 1000% looks like the wrong way to propagate the deadline to TrackedRead
return execute(requestTime, request, state, cm);
}
}
finally
@ -1149,7 +1292,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
}
}
static Response execute(AbstractRequest<?> request, PaxosState state, ClusterMetadata cm)
static Future<Response> execute(RequestTime requestTime, AbstractRequest<?> request, PaxosState state, ClusterMetadata cm)
{
MaybePromise result = state.promiseIfNewer(request.ballot, request.isForWrite);
switch (result.outcome)
@ -1165,7 +1308,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
Map<InetAddressAndPort, EndpointState> gossipInfo = verifyElectorate(request.electorate, localElectorate);
// TODO when 6.0 is the minimum supported version we can modify verifyElectorate to just return this epoch
Epoch electorateEpoch = gossipInfo.isEmpty() ? Epoch.EMPTY : localElectorate.createdAt;
ReadResponse readResponse = null;
Future<? extends IReadResponse> readResponseFuture = NO_READ_RESPONSE;
// Check we cannot race with a proposal, i.e. that we have not made a promise that
// could be in the process of making a proposal. If a majority of nodes have made no such promise
@ -1179,7 +1322,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
Ballot mostRecentCommit = result.before.accepted != null
&& result.before.accepted.ballot.compareTo(result.before.committed.ballot) > 0
&& result.before.accepted.update.isEmpty()
&& result.before.accepted.isEmpty()
? result.before.accepted.ballot : result.before.committed.ballot;
boolean hasProposalStability = mostRecentCommit.equals(result.before.promisedWrite)
@ -1187,15 +1330,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
if (request.read != null)
{
SinglePartitionReadCommand readCommand = request.read;
// Allows txn recovery to read even if it would be blocked by migration away from Paxos
if (request.isForRecovery)
readCommand = readCommand.withTransactionalSettings(false, readCommand.nowInSec());
try (ReadExecutionController executionController = readCommand.executionController();
UnfilteredPartitionIterator iterator = readCommand.executeLocally(executionController, cm))
{
readResponse = readCommand.createResponse(iterator, executionController.getRepairedDataInfo());
}
readResponseFuture = request.read.isTracked() ? readTracked((TrackedRead.Request)request.read, requestTime, cm) : readUntracked((SinglePartitionReadCommand)request.read, request.isForRecovery);
if (hasProposalStability)
{
@ -1212,20 +1347,56 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
ColumnFamilyStore cfs = Schema.instance.getColumnFamilyStoreInstance(request.table.id);
long lowBound = cfs.getPaxosRepairLowBound(request.partitionKey).uuidTimestamp();
return new Permitted(result.outcome, lowBound, acceptedButNotCommitted, committed, readResponse, hasProposalStability, gossipInfo, electorateEpoch, supersededBy);
boolean hasProposalStabilityFinal = hasProposalStability;
return readResponseFuture.map(readResponse -> new Permitted(result.outcome, lowBound, acceptedButNotCommitted, committed, readResponse, hasProposalStabilityFinal, gossipInfo, electorateEpoch, supersededBy));
case REJECT:
return new Rejected(result.supersededBy());
return ImmediateFuture.success(new Rejected(result.supersededBy()));
default:
throw new IllegalStateException();
}
}
private static Future<? extends IReadResponse> readTracked(TrackedRead.Request read, RequestTime requestTime, ClusterMetadata cm)
{
// TODO(accord): This doesn't honor reads for recovery when going down the tracked path which Accord needs
if (read.kind() == TRACKED_DATA)
return readTrackedData((DataRequest)read, requestTime, cm);
else
{
return readTrackedSummary((SummaryRequest)read, requestTime, cm);
}
}
private static Future<? extends IReadResponse> readTrackedSummary(SummaryRequest read, RequestTime requestTime, ClusterMetadata cm)
{
return read.executeLocally(read, cm, requestTime);
}
private static Future<? extends IReadResponse> readTrackedData(DataRequest read, RequestTime requestTime, ClusterMetadata cm)
{
return read.executeLocally(read, cm, requestTime);
}
private static Future<? extends IReadResponse> readUntracked(SinglePartitionReadCommand read, boolean isForRecovery)
{
// Allows txn recovery to read even if it would be blocked by migration away from Paxos
if (isForRecovery)
read = read.withTransactionalSettings(false, read.nowInSec());
ReadResponse readResponse;
try (ReadExecutionController executionController = read.executionController();
UnfilteredPartitionIterator iterator = read.executeLocally(executionController))
{
readResponse = read.createResponse(iterator, executionController.getRepairedDataInfo());
}
return ImmediateFuture.success(readResponse);
}
}
static abstract class AbstractRequestSerializer<R extends AbstractRequest<R>, T> implements IVersionedSerializer<R>
{
abstract R construct(T param, Ballot ballot, Electorate electorate, SinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery);
abstract R construct(T param, Ballot ballot, Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery);
abstract R construct(T param, Ballot ballot, Electorate electorate, DecoratedKey partitionKey, TableMetadata table, boolean isWrite, boolean isForRecovery);
@Override
@ -1237,7 +1408,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
if (request.read != null)
{
ReadCommand.serializer.serialize(request.read, out, version);
EmbeddableSinglePartitionReadCommand.serializer.serialize(request.read, out, version);
}
else
{
@ -1255,7 +1426,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
byte flag = in.readByte();
if ((flag & 1) != 0)
{
SinglePartitionReadCommand readCommand = (SinglePartitionReadCommand) ReadCommand.serializer.deserialize(in, version);
EmbeddableSinglePartitionReadCommand readCommand = EmbeddableSinglePartitionReadCommand.serializer.deserialize(in, version);
boolean isForRecovery = false;
if (version >= MessagingService.VERSION_60)
isForRecovery = in.readBoolean();
@ -1278,7 +1449,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
long size = Ballot.sizeInBytes()
+ Electorate.serializer.serializedSize(request.electorate, version)
+ 1 + (request.read != null
? ReadCommand.serializer.serializedSize(request.read, version)
? EmbeddableSinglePartitionReadCommand.serializer.serializedSize(request.read, version)
: request.table.id.serializedSize()
+ DecoratedKey.serializer.serializedSize(request.partitionKey, version));
if (version >= MessagingService.VERSION_60)
@ -1289,7 +1460,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
public static class RequestSerializer extends AbstractRequestSerializer<Request, Object>
{
Request construct(Object ignore, Ballot ballot, Electorate electorate, SinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery)
Request construct(Object ignore, Ballot ballot, Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery)
{
return new Request(ballot, electorate, read, isWrite, isForRecovery);
}
@ -1329,7 +1500,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
Accepted.serializer.serialize(promised.latestAcceptedButNotCommitted, out, version);
Committed.serializer.serialize(promised.latestCommitted, out, version);
if (promised.readResponse != null)
ReadResponse.serializer.serialize(promised.readResponse, out, version);
IReadResponse.serializer.serialize(promised.readResponse, out, version);
serializeMap(promised.gossipInfo, out, version, inetAddressAndPortSerializer, EndpointState.nullableSerializer);
if (version >= MessagingService.VERSION_60)
Epoch.messageSerializer.serialize(promised.electorateEpoch, out, version);
@ -1351,7 +1522,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
long lowBound = in.readUnsignedVInt();
Accepted acceptedNotCommitted = (flags & 2) != 0 ? Accepted.serializer.deserialize(in, version) : null;
Committed committed = Committed.serializer.deserialize(in, version);
ReadResponse readResponse = (flags & 4) != 0 ? ReadResponse.serializer.deserialize(in, version) : null;
IReadResponse readResponse = (flags & 4) != 0 ? IReadResponse.serializer.deserialize(in, version) : null;
Map<InetAddressAndPort, EndpointState> gossipInfo = deserializeMap(in, version, inetAddressAndPortSerializer, EndpointState.nullableSerializer);
Epoch electorateEpoch = version >= MessagingService.VERSION_60 ? Epoch.messageSerializer.deserialize(in, version) : Epoch.EMPTY;
MaybePromise.Outcome outcome = (flags & 16) != 0 ? PERMIT_READ : PROMISE;
@ -1376,7 +1547,7 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
size += VIntCoding.computeUnsignedVIntSize(permitted.lowBound)
+ (permitted.latestAcceptedButNotCommitted == null ? 0 : Accepted.serializer.serializedSize(permitted.latestAcceptedButNotCommitted, version))
+ Committed.serializer.serializedSize(permitted.latestCommitted, version)
+ (permitted.readResponse == null ? 0 : ReadResponse.serializer.serializedSize(permitted.readResponse, version))
+ (permitted.readResponse == null ? 0 : IReadResponse.serializer.serializedSize(permitted.readResponse, version))
+ serializedMapSize(permitted.gossipInfo, version, inetAddressAndPortSerializer, EndpointState.nullableSerializer)
+ (version >= MessagingService.VERSION_60 ? Epoch.messageSerializer.serializedSize(permitted.electorateEpoch, version) : 0)
+ (permitted.outcome == PERMIT_READ ? Ballot.sizeInBytes() : 0);
@ -1394,6 +1565,22 @@ public class PaxosPrepare extends PaxosRequestCallback<PaxosPrepare.Response> im
return send.withPayload(send.payload.withoutRead());
}
static <R extends AbstractRequest<R>> Message<R> withTrackedDataRequest(Message<R> send, Id id, ConsistencyLevel cl, int dataNode, int[] summaryNodes)
{
if (send.payload.read == null)
return send;
return send.withPayload(send.payload.asTrackedDataRequest(id, cl, dataNode, summaryNodes));
}
static <R extends AbstractRequest<R>> Message<R> withTrackedSummaryRequest(Message<R> send, Id id, int dataNode, int[] summaryNodes)
{
if (send.payload.read == null)
return send;
return send.withPayload(send.payload.asTrackedSummaryRequest(id, dataNode, summaryNodes));
}
public static void setOnLinearizabilityViolation(Runnable runnable)
{
assert onLinearizabilityViolation == null || runnable == null;

View File

@ -24,18 +24,24 @@ import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.RequestCallbackWithFailure;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.service.paxos.Commit.Agreed;
import org.apache.cassandra.service.paxos.Commit.Committed;
import org.apache.cassandra.tcm.ClusterMetadata;
@ -72,17 +78,113 @@ public class PaxosPrepareRefresh implements RequestCallbackWithFailure<PaxosPrep
void onRefreshSuccess(Ballot isSupersededBy, InetAddressAndPort from);
}
private final Message<Request> send;
private Message<Request> send;
private final Callbacks callbacks;
private final Paxos.Participants participants;
private final boolean isUrgent;
public PaxosPrepareRefresh(Ballot prepared, Paxos.Participants participants, Committed latestCommitted, Callbacks callbacks)
{
this.callbacks = callbacks;
this.send = Message.out(PAXOS2_PREPARE_REFRESH_REQ, new Request(prepared, latestCommitted), participants.isUrgent());
this.participants = participants;
this.isUrgent = participants.isUrgent();
this.send = Message.out(PAXOS2_PREPARE_REFRESH_REQ, new Request(prepared, latestCommitted), isUrgent);
}
/*
* onRefreshSuccess can be called from this method synchronously due to local application at this node
* so the caller must be aware that in PaxosPrepare completion may already have been signaled.
*/
void refresh(List<InetAddressAndPort> refresh)
{
// Check if forwarding is needed for tracked keyspaces
Committed commit = send.payload.missingCommit;
if (commit.metadata().replicationType().isTracked() && commit.mutation.id().isNone())
{
// Check if we can generate mutation ID locally (are we a replica?)
Replica localReplica = participants.all.byEndpoint().get(getBroadcastAddressAndPort());
if (localReplica != null)
{
// We ARE a replica - generate mutation ID and update the commit
String keyspaceName = commit.metadata().keyspace;
MutationId mutationId = MutationTrackingService.instance.nextMutationId(keyspaceName, commit.partitionKey().getToken());
Mutation mutationWithId = commit.makeMutation(mutationId);
Committed commitWithId = new Commit.Committed(commit.ballot, mutationWithId);
// Update the message payload with the new commit
this.send = Message.out(PAXOS2_PREPARE_REFRESH_REQ,
new Request(send.payload.promised, commitWithId),
isUrgent);
// For tracked keyspaces, we MUST ALWAYS write to the local journal since we generated the mutation ID.
// This is required for retry purposes: if a remote target fails, the ActiveLogReconciler will try
// to look up the mutation in the local journal. The node that generated the mutation ID is the "owner"
// and must have the mutation available for retries.
//
// We do this BEFORE the main refresh loop to ensure the mutation is in the journal before any
// failure callback can trigger reconciliation.
Response localResponse = null;
try
{
localResponse = RequestHandler.execute(this.send.payload, getBroadcastAddressAndPort());
if (localResponse == null)
logger.warn("Local execution failed for tracked mutation {}", mutationId);
}
catch (Exception e)
{
logger.warn("Exception writing tracked mutation {} locally", mutationId, e);
}
// If self is in the refresh list, report the local execution result to callbacks
// We need to do this because the main loop will skip self for tracked keyspaces
for (int i = 0, size = refresh.size(); i < size; ++i)
{
if (shouldExecuteOnSelf(refresh.get(i)))
{
if (localResponse != null)
callbacks.onRefreshSuccess(localResponse.isSupersededBy, getBroadcastAddressAndPort());
else
callbacks.onRefreshFailure(getBroadcastAddressAndPort(), RequestFailure.UNKNOWN);
break;
}
}
}
else
{
// We're NOT a replica - forward to a replica
forwardRefresh(refresh);
return;
}
}
// For tracked keyspaces where we generated the ID above, we already wrote locally.
// For tracked keyspaces where the ID was already present, we still need to ensure local execution.
boolean isTracked = !send.payload.missingCommit.mutation.id().isNone();
// If we just generated the ID above, we already wrote locally - check by examining the original commit
boolean alreadyWroteLocally = commit.metadata().replicationType().isTracked()
&& commit.mutation.id().isNone()
&& participants.all.byEndpoint().get(getBroadcastAddressAndPort()) != null;
boolean localExecutedSync = alreadyWroteLocally;
// For tracked keyspaces where we DIDN'T generate the ID (it was already present), we still need to
// execute locally BEFORE sending to remotes if self is in the refresh list.
if (isTracked && !alreadyWroteLocally)
{
for (int i = 0, size = refresh.size(); i < size; ++i)
{
if (shouldExecuteOnSelf(refresh.get(i)))
{
executeOnSelf(); // SYNCHRONOUS - journal write completes here
localExecutedSync = true;
break;
}
}
}
// Now send to remote nodes (and record local execution for non-tracked keyspaces)
boolean executeOnSelf = false;
for (int i = 0, size = refresh.size(); i < size ; ++i)
{
@ -95,15 +197,104 @@ public class PaxosPrepareRefresh implements RequestCallbackWithFailure<PaxosPrep
Tracing.trace("Refresh {} and Confirm {} to {}", send.payload.missingCommit.ballot, send.payload.promised, destination);
if (shouldExecuteOnSelf(destination))
executeOnSelf = true;
{
// For tracked keyspaces, skip self - already executed synchronously above
if (!localExecutedSync)
executeOnSelf = true;
}
else
{
MessagingService.instance().sendWithCallback(send, destination, this);
}
}
// Async local execution only for non-tracked keyspaces
if (executeOnSelf)
PAXOS2_PREPARE_REFRESH_REQ.stage.execute(this::executeOnSelf);
}
/**
* Forward the refresh operation to a replica coordinator.
* The replica will generate the mutation ID and send to all refresh nodes.
*/
private void forwardRefresh(List<InetAddressAndPort> refreshTargets)
{
// Find a live replica to forward to (that's not us)
InetAddressAndPort targetReplica = null;
for (Replica replica : participants.all)
{
if (!replica.endpoint().equals(getBroadcastAddressAndPort()) &&
FailureDetector.instance.isAlive(replica.endpoint()))
{
targetReplica = replica.endpoint();
break;
}
}
if (targetReplica == null)
{
logger.error("No live replica available to forward PaxosPrepareRefresh for {}",
send.payload.missingCommit.partitionKey());
// Report failure for all refresh targets
for (InetAddressAndPort target : refreshTargets)
callbacks.onRefreshFailure(target, RequestFailure.UNKNOWN);
return;
}
logger.debug("Forwarding PaxosPrepareRefresh to replica {} for mutation ID generation", targetReplica);
Tracing.trace("Forwarding PaxosPrepareRefresh to replica {}", targetReplica);
// Create forward request with refresh targets
PrepareRefreshForwardRequest forwardRequest = new PrepareRefreshForwardRequest(
send.payload.promised,
send.payload.missingCommit,
refreshTargets,
isUrgent
);
Message<PrepareRefreshForwardRequest> message = Message.out(
Verb.PAXOS_PREPARE_REFRESH_FORWARD_REQ, forwardRequest, isUrgent);
// Send and handle response
MessagingService.instance().sendWithCallback(message, targetReplica,
new ForwardCallback(refreshTargets));
}
/**
* Callback for forwarded refresh operations.
* Translates forward response to individual refresh callbacks.
*/
private class ForwardCallback implements RequestCallbackWithFailure<PrepareRefreshForwardResponse>
{
private final List<InetAddressAndPort> refreshTargets;
ForwardCallback(List<InetAddressAndPort> refreshTargets)
{
this.refreshTargets = refreshTargets;
}
@Override
public void onResponse(Message<PrepareRefreshForwardResponse> message)
{
PrepareRefreshForwardResponse response = message.payload;
// Report results for each target
for (int i = 0; i < refreshTargets.size(); i++)
{
InetAddressAndPort target = refreshTargets.get(i);
Ballot supersededBy = response.supersededBy.get(i);
callbacks.onRefreshSuccess(supersededBy, target);
}
}
@Override
public void onFailure(InetAddressAndPort from, RequestFailure reason)
{
// Report failure for all targets
for (InetAddressAndPort target : refreshTargets)
callbacks.onRefreshFailure(target, reason);
}
}
@Override
public void onFailure(InetAddressAndPort from, RequestFailure reason)
{
@ -148,7 +339,7 @@ public class PaxosPrepareRefresh implements RequestCallbackWithFailure<PaxosPrep
callbacks.onRefreshSuccess(response.isSupersededBy, from);
}
private static class Request
static class Request
{
final Ballot promised;
final Committed missingCommit;
@ -193,7 +384,7 @@ public class PaxosPrepareRefresh implements RequestCallbackWithFailure<PaxosPrep
{
Agreed commit = request.missingCommit;
if (!Paxos.isInRangeAndShouldProcess(from, commit.update.partitionKey(), commit.update.metadata(), false))
if (!Paxos.isInRangeAndShouldProcess(commit.partitionKey(), commit.metadata(), false))
return null;
try (PaxosState state = PaxosState.get(commit))

View File

@ -172,12 +172,10 @@ public class PaxosPropose<OnDone extends Consumer<? super PaxosPropose.Status>>
* or for the present status if the time elapses without a final result being reached.
* @param waitForNoSideEffect if true, on failure we will wait until we can say with certainty there are no side effects
* or until we know we will never be able to determine this with certainty
* @param isForRecovery if true the value being proposed is not a new value it is a value from an existing in flight proposal
* and will be allowed to proceed even if the key is migrating to a different consensus protocol
*/
static Paxos.Async<Status> propose(Proposal proposal, Paxos.Participants participants, boolean waitForNoSideEffect)
{
if (waitForNoSideEffect && proposal.update.isEmpty())
if (waitForNoSideEffect && proposal.isEmpty())
waitForNoSideEffect = false; // by definition this has no "side effects" (besides linearizing the operation)
// to avoid unnecessary object allocations we extend PaxosPropose to implements Paxos.Async
@ -211,7 +209,7 @@ public class PaxosPropose<OnDone extends Consumer<? super PaxosPropose.Status>>
static <T extends Consumer<Status>> T propose(Proposal proposal, Paxos.Participants participants, boolean waitForNoSideEffect, T onDone)
{
if (waitForNoSideEffect && proposal.update.isEmpty())
if (waitForNoSideEffect && proposal.isEmpty())
waitForNoSideEffect = false; // by definition this has no "side effects" (besides linearizing the operation)
PaxosPropose<?> propose = new PaxosPropose<>(proposal, participants.sizeOfPoll(), participants.sizeOfConsensusQuorum, waitForNoSideEffect, onDone);
@ -416,7 +414,7 @@ public class PaxosPropose<OnDone extends Consumer<? super PaxosPropose.Status>>
try
{
AcceptResult acceptResult = execute(message.payload.proposal, message.from());
AcceptResult acceptResult = execute(message.payload.proposal);
if (acceptResult == null)
MessagingService.instance().respondWithFailure(UNKNOWN, message);
else
@ -429,9 +427,9 @@ public class PaxosPropose<OnDone extends Consumer<? super PaxosPropose.Status>>
}
}
public static AcceptResult execute(Proposal proposal, InetAddressAndPort from)
public static AcceptResult execute(Proposal proposal)
{
if (!Paxos.isInRangeAndShouldProcess(from, proposal.update.partitionKey(), proposal.update.metadata(), false))
if (!Paxos.isInRangeAndShouldProcess(proposal.partitionKey(), proposal.metadata(), false))
return null;
long start = nanoTime();
@ -441,7 +439,7 @@ public class PaxosPropose<OnDone extends Consumer<? super PaxosPropose.Status>>
}
finally
{
Keyspace.openAndGetStore(proposal.update.metadata()).metric.casPropose.addNano(nanoTime() - start);
Keyspace.openAndGetStore(proposal.metadata()).metric.casPropose.addNano(nanoTime() - start);
}
}
}

View File

@ -395,7 +395,7 @@ public class PaxosRepair extends AbstractPaxosRepair
return retry(this);
case SUCCESS:
if (proposal.update.isEmpty())
if (proposal.isEmpty())
{
logger.trace("PaxosRepair of {} complete after successful empty proposal", partitionKey());
return DONE;
@ -603,7 +603,7 @@ public class PaxosRepair extends AbstractPaxosRepair
public void doVerb(Message<PaxosRepair.Request> message)
{
PaxosRepair.Request request = message.payload;
if (!isInRangeAndShouldProcess(message.from(), request.partitionKey, request.table, false))
if (!isInRangeAndShouldProcess(request.partitionKey, request.table, false))
{
MessagingService.instance().respondWithFailure(UNKNOWN, message);
return;

View File

@ -19,6 +19,7 @@
package org.apache.cassandra.service.paxos;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -32,10 +33,12 @@ import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.service.FailureRecordingCallback;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.utils.TriFunction;
import org.apache.cassandra.utils.concurrent.Future;
import static org.apache.cassandra.exceptions.RequestFailure.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
import static org.apache.cassandra.exceptions.RequestFailure.TIMEOUT;
import static org.apache.cassandra.exceptions.RequestFailure.UNKNOWN;
import static com.google.common.util.concurrent.Futures.getUnchecked;
import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort;
public abstract class PaxosRequestCallback<T> extends FailureRecordingCallback<T>
@ -53,12 +56,12 @@ public abstract class PaxosRequestCallback<T> extends FailureRecordingCallback<T
onResponse(message.payload, message.from());
}
protected <I> void executeOnSelf(I parameter, BiFunction<I, InetAddressAndPort, T> execute)
protected <I> void executeOnSelf(I parameter, Function<I, T> execute)
{
T response;
try
{
response = execute.apply(parameter, getBroadcastAddressAndPort());
response = execute.apply(parameter);
if (response == null)
return;
}
@ -80,35 +83,59 @@ public abstract class PaxosRequestCallback<T> extends FailureRecordingCallback<T
onResponse(response, getBroadcastAddressAndPort());
}
protected <I, J> void executeOnSelf(I parameter1, J parameter2, TriFunction<I, J, InetAddressAndPort, T> execute)
protected <I, J> void executeOnSelfAsync(I parameter1, J parameter2, BiFunction<I, J, Future<T>> execute)
{
T response;
try
{
response = execute.apply(parameter1, parameter2, getBroadcastAddressAndPort());
if (response == null)
Future<T> responseFuture = execute.apply(parameter1, parameter2);
if (responseFuture == null)
return;
}
catch (RetryOnDifferentSystemException e)
{
onFailure(getBroadcastAddressAndPort(), RequestFailure.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM);
return;
if (responseFuture.isDone())
{
// Fast path: future already complete
T response = getUnchecked(responseFuture);
onResponse(response, getBroadcastAddressAndPort());
}
else
{
// Async path: add callback for when future completes
responseFuture.addCallback((response, failure) -> {
if (failure != null)
{
RequestFailure reason = UNKNOWN;
if (failure instanceof WriteTimeoutException)
reason = TIMEOUT;
else if (failure instanceof RetryOnDifferentSystemException)
reason = RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
else
logger.error("Failed to apply {} locally", parameter1, failure);
onFailure(getBroadcastAddressAndPort(), reason);
}
else
{
onResponse(response, getBroadcastAddressAndPort());
}
});
}
}
catch (Exception ex)
{
RequestFailure reason = UNKNOWN;
if (ex instanceof WriteTimeoutException) reason = TIMEOUT;
else logger.error("Failed to apply {}, {} locally", parameter1, parameter2, ex);
if (ex instanceof WriteTimeoutException)
reason = TIMEOUT;
else if (ex instanceof RetryOnDifferentSystemException)
reason = RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
else
logger.error("Failed to apply {} locally", parameter1, ex);
onFailure(getBroadcastAddressAndPort(), reason);
return;
}
onResponse(response, getBroadcastAddressAndPort());
}
static boolean shouldExecuteOnSelf(InetAddressAndPort replica)
{
return USE_SELF_EXECUTION && replica.equals(getBroadcastAddressAndPort());
}
}
}

View File

@ -51,6 +51,9 @@ import org.apache.cassandra.exceptions.ReadTimeoutException;
import org.apache.cassandra.exceptions.RequestTimeoutException;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.metrics.PaxosMetrics;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState;
import org.apache.cassandra.service.paxos.uncommitted.PaxosBallotTracker;
@ -191,8 +194,8 @@ public class PaxosState implements PaxosOperationLock
public Snapshot(@Nonnull Ballot promised, @Nonnull Ballot promisedWrite, @Nullable Accepted accepted, @Nonnull Committed committed)
{
assert isAfter(promised, promisedWrite) || promised == promisedWrite;
assert accepted == null || accepted.update.partitionKey().equals(committed.update.partitionKey());
assert accepted == null || accepted.update.metadata().id.equals(committed.update.metadata().id);
assert accepted == null || accepted.partitionKey().equals(committed.partitionKey());
assert accepted == null || accepted.metadata().id.equals(committed.metadata().id);
assert accepted == null || committed.isBefore(accepted.ballot);
this.promised = promised;
@ -224,7 +227,7 @@ public class PaxosState implements PaxosOperationLock
{
// warn: if proposal has same timestamp as promised, we should prefer accepted
// since (if different) it reached a quorum of promises; this means providing it as first argument
Ballot latest = accepted != null && !accepted.update.isEmpty() ? accepted.ballot : null;
Ballot latest = accepted != null && !accepted.isEmpty() ? accepted.ballot : null;
latest = latest(latest, committed.ballot);
latest = latest(latest, promisedWrite);
latest = latest(latest, ballotTracker().getLowBound());
@ -272,7 +275,7 @@ public class PaxosState implements PaxosOperationLock
if (paxosStatePurging() == gc_grace)
{
long expireOlderThan = SECONDS.toMicros(nowInSec - committed.update.metadata().params.gcGraceSeconds);
long expireOlderThan = SECONDS.toMicros(nowInSec - committed.metadata().params.gcGraceSeconds);
isAcceptedExpired |= accepted != null && accepted.ballot.unixMicros() < expireOlderThan;
isCommittedExpired |= committed.ballot.unixMicros() < expireOlderThan;
}
@ -283,7 +286,7 @@ public class PaxosState implements PaxosOperationLock
return new Snapshot(promised, promisedWrite,
isAcceptedExpired ? null : accepted,
isCommittedExpired
? Committed.none(committed.update.partitionKey(), committed.update.metadata())
? Committed.none(committed.partitionKey(), committed.metadata())
: committed);
}
}
@ -298,7 +301,7 @@ public class PaxosState implements PaxosOperationLock
public UnsafeSnapshot(@Nonnull Commit committed)
{
this(new Committed(committed.ballot, committed.update));
this(new Committed(committed.ballot, committed.mutation));
}
}
@ -365,7 +368,7 @@ public class PaxosState implements PaxosOperationLock
@VisibleForTesting
public static PaxosState get(Commit commit)
{
return get(commit.update.partitionKey(), commit.update.metadata());
return get(commit.partitionKey(), commit.metadata());
}
public static PaxosState get(DecoratedKey partitionKey, TableMetadata table)
@ -406,7 +409,7 @@ public class PaxosState implements PaxosOperationLock
private static PaxosState getUnsafe(Commit commit)
{
return getUnsafe(commit.update.partitionKey(), commit.update.metadata());
return getUnsafe(commit.partitionKey(), commit.metadata());
}
// don't increment the total count, as we are only using this for locking purposes when coordinating
@ -696,7 +699,7 @@ public class PaxosState implements PaxosOperationLock
public static void commitDirect(Commit commit)
{
applyCommit(commit, null, (apply, ignore) -> {
try (PaxosState state = tryGetUnsafe(apply.update.partitionKey(), apply.update.metadata()))
try (PaxosState state = tryGetUnsafe(apply.partitionKey(), apply.metadata()))
{
if (state != null)
currentUpdater.accumulateAndGet(state, new UnsafeSnapshot(apply), Snapshot::merge);
@ -715,7 +718,7 @@ public class PaxosState implements PaxosOperationLock
// TODO: run Paxos Repair before truncate so we can excise this
// The table may have been truncated since the proposal was initiated. In that case, we
// don't want to perform the mutation and potentially resurrect truncated data
if (commit.ballot.unixMicros() >= SystemKeyspace.getTruncatedAt(commit.update.metadata().id))
if (commit.ballot.unixMicros() >= SystemKeyspace.getTruncatedAt(commit.metadata().id))
{
Tracing.trace("Committing proposal {}", commit);
Mutation mutation = commit.makeMutation();
@ -724,6 +727,21 @@ public class PaxosState implements PaxosOperationLock
else
{
Tracing.trace("Not committing proposal {} as ballot timestamp predates last truncation time", commit);
// Still acknowledge mutation ID for tracked keyspaces even though we're discarding
// This ensures the tracking service knows this replica "handled" the mutation.
// We call both startWriting and finishWriting - startWriting registers the mutation
// data (for reconciliation) and finishWriting marks it as witnessed.
// This is needed because there are cases where mutation IDs might be created for the same Paxos
// commit.
Mutation mutation = commit.makeMutation();
if (!mutation.id().isNone())
{
KeyspaceMetadata ksm = Schema.instance.getKeyspaceMetadata(mutation.getKeyspaceName());
if (ksm != null && ksm.params.replicationType.isTracked()
&& MutationTrackingService.instance.startWriting(mutation))
MutationTrackingService.instance.finishWriting(mutation);
}
}
// for commits we save to disk first, because we can; even here though it is safe to permit later events to
@ -737,7 +755,7 @@ public class PaxosState implements PaxosOperationLock
}
finally
{
Keyspace.openAndGetStore(commit.update.metadata()).metric.casCommit.addNano(nanoTime() - start);
Keyspace.openAndGetStore(commit.metadata()).metric.casCommit.addNano(nanoTime() - start);
}
}
@ -764,8 +782,8 @@ public class PaxosState implements PaxosOperationLock
if (currentUpdater.compareAndSet(unsafeState, realBefore, after))
{
Tracing.trace("Promising ballot {}", toPrepare.ballot);
DecoratedKey partitionKey = toPrepare.update.partitionKey();
TableMetadata metadata = toPrepare.update.metadata();
DecoratedKey partitionKey = toPrepare.partitionKey();
TableMetadata metadata = toPrepare.metadata();
SystemKeyspace.savePaxosWritePromise(partitionKey, metadata, toPrepare.ballot);
return new PrepareResponse(true, before.accepted == null ? Accepted.none(partitionKey, metadata) : before.accepted, before.committed);
}
@ -774,14 +792,14 @@ public class PaxosState implements PaxosOperationLock
{
Tracing.trace("Promise rejected; {} is not sufficiently newer than {}", toPrepare, before.promised);
// return the currently promised ballot (not the last accepted one) so the coordinator can make sure it uses newer ballot next time (#5667)
return new PrepareResponse(false, new Commit(before.promised, toPrepare.update), before.committed);
return new PrepareResponse(false, Commit.create(before.promised, toPrepare.mutation), before.committed);
}
}
}
}
finally
{
Keyspace.openAndGetStore(toPrepare.update.metadata()).metric.casPrepare.addNano(nanoTime() - start);
Keyspace.openAndGetStore(toPrepare.metadata()).metric.casPrepare.addNano(nanoTime() - start);
}
}
@ -827,7 +845,7 @@ public class PaxosState implements PaxosOperationLock
}
finally
{
Keyspace.openAndGetStore(proposal.update.metadata()).metric.casPropose.addNano(nanoTime() - start);
Keyspace.openAndGetStore(proposal.metadata()).metric.casPropose.addNano(nanoTime() - start);
}
}

View File

@ -0,0 +1,184 @@
/*
* 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.service.paxos;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.RequestCallbackWithFailure;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.service.paxos.Commit.Committed;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.concurrent.CountDownLatch;
import static org.apache.cassandra.net.Verb.PAXOS2_PREPARE_REFRESH_REQ;
import static org.apache.cassandra.service.paxos.PaxosRequestCallback.shouldExecuteOnSelf;
/**
* Handler for forwarded PaxosPrepareRefresh requests.
* Generates a mutation ID and sends the refresh to all target nodes.
*
* This handler is invoked when a non-replica coordinator forwards the refresh
* to a full replica that can generate the mutation ID.
*
* TODO (expected): more comprehensive testing
*/
public class PrepareRefreshForwardHandler implements IVerbHandler<PrepareRefreshForwardRequest>
{
public static final PrepareRefreshForwardHandler instance = new PrepareRefreshForwardHandler();
private static final Logger logger = LoggerFactory.getLogger(PrepareRefreshForwardHandler.class);
@Override
public void doVerb(Message<PrepareRefreshForwardRequest> message)
{
ClusterMetadataService.instance().fetchLogFromPeerOrCMS(message.from(), message.header.epoch);
PrepareRefreshForwardRequest request = message.payload;
Tracing.trace("Executing forwarded PaxosPrepareRefresh for {}", request.commit.partitionKey());
try
{
String ksName = request.commit.metadata().keyspace;
KeyspaceMetadata ksMetadata = Schema.instance.getKeyspaceMetadata(ksName);
if (ksMetadata == null)
{
MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message);
logger.error("Failed to forward paxos prepare refresh for non-existent keyspace {}", ksName);
return;
}
if (!ksMetadata.params.replicationType.isTracked())
{
MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message);
logger.error("Asked to perform forwarded prepare refresh, but keyspace {} is not tracked", ksName);
return;
}
Token token = request.commit.partitionKey().getToken();
MutationId mutationId = MutationTrackingService.instance.nextMutationId(ksName, token);
Mutation mutationWithId = request.commit.makeMutation(mutationId);
Committed commitWithId = new Commit.Committed(request.commit.ballot, mutationWithId);
// Now send the refresh to all targets and collect responses
List<InetAddressAndPort> targets = request.refreshTargets;
List<Ballot> supersededBy = Collections.synchronizedList(new ArrayList<>(Collections.nCopies(targets.size(), null)));
CountDownLatch latch = CountDownLatch.newCountDownLatch(targets.size());
Message<PaxosPrepareRefresh.Request> refreshMsg = Message.out(
PAXOS2_PREPARE_REFRESH_REQ,
new PaxosPrepareRefresh.Request(request.promised, commitWithId),
request.isUrgent
);
// For tracked keyspaces, we MUST ALWAYS write to the local journal since we generated the mutation ID.
// This is required for retry purposes: if a remote target fails, the ActiveLogReconciler will try
// to look up the mutation in the local journal. The node that generated the mutation ID is the "owner"
// and must have the mutation available for retries.
//
// This is different from checking if self is in targets - even if we're not in targets,
// we're still the ID generator and need the mutation locally.
try
{
PaxosPrepareRefresh.RequestHandler.execute(
new PaxosPrepareRefresh.Request(request.promised, commitWithId), FBUtilities.getBroadcastAddressAndPort());
// Note: we don't use the response since this node may not be in targets
}
catch (Exception e)
{
// Log but continue - we still need to send to targets
logger.warn("Failed to execute local commit for tracked keyspace mutation {}", mutationId, e);
}
// Now send to remote targets
for (int i = 0; i < targets.size(); i++)
{
final int targetIndex = i;
InetAddressAndPort target = targets.get(i);
// Check if self is in targets for response tracking (separate from the local write above)
// We need to decrement the latch for the local target since we already executed above
if (shouldExecuteOnSelf(target))
{
latch.decrement();
continue;
}
RequestCallbackWithFailure<PaxosPrepareRefresh.Response> callback = new RequestCallbackWithFailure<>()
{
@Override
public void onResponse(Message<PaxosPrepareRefresh.Response> response)
{
supersededBy.set(targetIndex, response.payload.isSupersededBy);
latch.decrement();
}
@Override
public void onFailure(InetAddressAndPort from, RequestFailure reason)
{
// Leave null to indicate we didn't get a definitive answer
latch.decrement();
}
};
MessagingService.instance().sendWithCallback(refreshMsg, target, callback);
}
// Wait for all responses with timeout
long timeoutNanos = message.expiresAtNanos() - Clock.Global.nanoTime();
boolean completed = latch.await(Math.max(0, timeoutNanos), TimeUnit.NANOSECONDS);
if (!completed)
logger.warn("Forwarded PaxosPrepareRefresh timed out waiting for responses");
// Send aggregated response back to original coordinator
MessagingService.instance().respond(new PrepareRefreshForwardResponse(supersededBy), message);
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
MessagingService.instance().respondWithFailure(RequestFailure.forException(e), message);
logger.error("Forwarded PaxosPrepareRefresh interrupted", e);
}
catch (Exception e)
{
MessagingService.instance().respondWithFailure(RequestFailure.forException(e), message);
logger.error("Failed to execute forwarded PaxosPrepareRefresh for {}", request.commit, e);
}
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.service.paxos;
import java.io.IOException;
import java.util.List;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.service.paxos.Commit.Committed;
import org.apache.cassandra.utils.CollectionSerializers;
/**
* Request to forward a PaxosPrepareRefresh operation to a full replica coordinator.
* This is used when the original coordinator is not a full replica but needs to
* execute a Paxos prepare refresh for a tracked keyspace that requires MutationId generation.
*
* The full replica coordinator will generate the mutation ID and send the refresh
* to all target nodes with the same mutation ID.
*/
public class PrepareRefreshForwardRequest
{
public static final Serializer serializer = new Serializer();
public final Ballot promised;
public final Committed commit;
public final List<InetAddressAndPort> refreshTargets;
public final boolean isUrgent;
public PrepareRefreshForwardRequest(Ballot promised, Committed commit, List<InetAddressAndPort> refreshTargets, boolean isUrgent)
{
this.promised = promised;
this.commit = commit;
this.refreshTargets = refreshTargets;
this.isUrgent = isUrgent;
}
public static class Serializer implements IVersionedSerializer<PrepareRefreshForwardRequest>
{
@Override
public void serialize(PrepareRefreshForwardRequest request, DataOutputPlus out, int version) throws IOException
{
request.promised.serialize(out);
Committed.serializer.serialize(request.commit, out, version);
CollectionSerializers.serializeList(request.refreshTargets, out, version, InetAddressAndPort.Serializer.inetAddressAndPortSerializer);
out.writeBoolean(request.isUrgent);
}
@Override
public PrepareRefreshForwardRequest deserialize(DataInputPlus in, int version) throws IOException
{
Ballot promised = Ballot.deserialize(in);
Committed commit = Committed.serializer.deserialize(in, version);
List<InetAddressAndPort> refreshTargets = CollectionSerializers.deserializeList(in, version, InetAddressAndPort.Serializer.inetAddressAndPortSerializer);
boolean isUrgent = in.readBoolean();
return new PrepareRefreshForwardRequest(promised, commit, refreshTargets, isUrgent);
}
@Override
public long serializedSize(PrepareRefreshForwardRequest request, int version)
{
long size = Ballot.sizeInBytes();
size += Committed.serializer.serializedSize(request.commit, version);
size += CollectionSerializers.serializedListSize(request.refreshTargets, version, InetAddressAndPort.Serializer.inetAddressAndPortSerializer);
size += TypeSizes.BOOL_SIZE; // isUrgent
return size;
}
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.service.paxos;
import java.io.IOException;
import java.util.List;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.utils.CollectionSerializers;
import org.apache.cassandra.utils.NullableSerializer;
/**
* Response from a forwarded PaxosPrepareRefresh operation.
* Contains the superseding ballot for each refresh target (null if confirmed).
*/
public class PrepareRefreshForwardResponse
{
public static final Serializer serializer = new Serializer();
/**
* List of superseding ballots, one per refresh target.
* Null entry means the promise was confirmed for that target.
*/
public final List<Ballot> supersededBy;
public PrepareRefreshForwardResponse(List<Ballot> supersededBy)
{
this.supersededBy = supersededBy;
}
public static class Serializer implements IVersionedSerializer<PrepareRefreshForwardResponse>
{
private static final IVersionedSerializer<Ballot> NULLABLE_BALLOT_SERIALIZER = NullableSerializer.wrap(Ballot.Serializer.instance);
@Override
public void serialize(PrepareRefreshForwardResponse response, DataOutputPlus out, int version) throws IOException
{
CollectionSerializers.serializeList(response.supersededBy, out, version, NULLABLE_BALLOT_SERIALIZER);
}
@Override
public PrepareRefreshForwardResponse deserialize(DataInputPlus in, int version) throws IOException
{
List<Ballot> supersededBy = CollectionSerializers.deserializeList(in, version, NULLABLE_BALLOT_SERIALIZER);
return new PrepareRefreshForwardResponse(supersededBy);
}
@Override
public long serializedSize(PrepareRefreshForwardResponse response, int version)
{
return CollectionSerializers.serializedListSize(response.supersededBy, version, NULLABLE_BALLOT_SERIALIZER);
}
}
}

View File

@ -44,8 +44,8 @@ public class PrepareResponse
public PrepareResponse(boolean promised, Commit inProgressCommit, Commit mostRecentCommit)
{
assert inProgressCommit.update.partitionKey().equals(mostRecentCommit.update.partitionKey());
assert inProgressCommit.update.metadata().id.equals(mostRecentCommit.update.metadata().id);
assert inProgressCommit.partitionKey().equals(mostRecentCommit.partitionKey());
assert inProgressCommit.metadata().id.equals(mostRecentCommit.metadata().id);
this.promised = promised;
this.mostRecentCommit = mostRecentCommit;

Some files were not shown because too many files have changed in this diff Show More