mirror of https://github.com/apache/cassandra
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:
parent
06e7dee9f8
commit
b11122f23b
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,4 +28,10 @@ public class AuthenticationException extends RequestValidationException
|
|||
{
|
||||
super(ExceptionCode.BAD_CREDENTIALS, msg, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraExceptionCode getCassandraExceptionCode()
|
||||
{
|
||||
return CassandraExceptionCode.BAD_CREDENTIALS;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,4 +23,10 @@ public class CDCWriteException extends RequestExecutionException
|
|||
{
|
||||
super(ExceptionCode.CDC_WRITE_FAILURE, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraExceptionCode getCassandraExceptionCode()
|
||||
{
|
||||
return CassandraExceptionCode.CDC_WRITE_FAILURE;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -49,4 +49,10 @@ public class ConfigurationException extends RequestValidationException
|
|||
super(code, msg);
|
||||
logStackTrace = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraExceptionCode getCassandraExceptionCode()
|
||||
{
|
||||
return CassandraExceptionCode.CONFIG_ERROR;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,4 +28,10 @@ public class InvalidRequestException extends RequestValidationException
|
|||
{
|
||||
super(ExceptionCode.INVALID, msg, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraExceptionCode getCassandraExceptionCode()
|
||||
{
|
||||
return CassandraExceptionCode.INVALID;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,4 +23,10 @@ public class OverloadedException extends RequestExecutionException
|
|||
{
|
||||
super(ExceptionCode.OVERLOADED, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraExceptionCode getCassandraExceptionCode()
|
||||
{
|
||||
return CassandraExceptionCode.OVERLOADED;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,10 @@ public class OversizedCQLMessageException extends InvalidRequestException
|
|||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraExceptionCode getCassandraExceptionCode()
|
||||
{
|
||||
return CassandraExceptionCode.OVERSIZED_MESSAGE;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -23,4 +23,10 @@ public class SyntaxException extends RequestValidationException
|
|||
{
|
||||
super(ExceptionCode.SYNTAX_ERROR, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraExceptionCode getCassandraExceptionCode()
|
||||
{
|
||||
return CassandraExceptionCode.SYNTAX_ERROR;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,4 +28,10 @@ public class TruncateException extends RequestExecutionException
|
|||
{
|
||||
super(ExceptionCode.TRUNCATE_ERROR, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraExceptionCode getCassandraExceptionCode()
|
||||
{
|
||||
return CassandraExceptionCode.TRUNCATE_ERROR;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,4 +28,10 @@ public class UnauthorizedException extends RequestValidationException
|
|||
{
|
||||
super(ExceptionCode.UNAUTHORIZED, msg, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraExceptionCode getCassandraExceptionCode()
|
||||
{
|
||||
return CassandraExceptionCode.UNAUTHORIZED;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue