mirror of https://github.com/apache/cassandra
Merge branch 'cassandra-4.0' into cassandra-4.1
* cassandra-4.0: Safer handling of out-of-range tokens
This commit is contained in:
commit
8670d98498
|
|
@ -6,6 +6,7 @@
|
|||
* Reduce info logging from automatic paxos repair (CASSANDRA-19445)
|
||||
* Support legacy plain_text_auth section in credentials file removed unintentionally (CASSANDRA-19498)
|
||||
Merged from 4.0:
|
||||
* Safer handling of out-of-range tokens (CASSANDRA-13704)
|
||||
* Fix memory leak in BTree.FastBuilder (CASSANDRA-19785)
|
||||
* Fix millisecond and microsecond precision for commit log replay (CASSANDRA-19448)
|
||||
* Improve accuracy of memtable heap usage tracking (CASSANDRA-17298)
|
||||
|
|
|
|||
12
NEWS.txt
12
NEWS.txt
|
|
@ -60,6 +60,18 @@ Upgrading
|
|||
improved management of pending requests; any custom implementations
|
||||
(using the cassandra.custom_query_handler_class system property) will
|
||||
need to be recompiled. For further details, see CASSANDRA-19534.
|
||||
- This release introduces safeguards and observability into possible data loss scenarios when nodes have a
|
||||
divergent view of the cluster. This happens around edge-cases on unsafe bootstrapping, decommissions, or when a
|
||||
node has a corrupted topology. Two configuration options have been added: `log_out_of_token_range_requests` and
|
||||
`reject_out_of_token_range_requests`, both enabled by default. The former will make nodes log requests they
|
||||
receive that don't belong in their current or pending token ranges. The latter will reject those requests
|
||||
altogether, which prevents the vast majority of eventual data loss that can occur, but may also incur small
|
||||
windows of degraded availability during range movements in an unhealthy cluster. Both options apply to streaming,
|
||||
mutations, hints, single-partition reads, read-repair, and Paxos operations. Logging these requests is recommended
|
||||
in all situations, and operators are encouraged to be aware when such events occur and to investigate if writes
|
||||
have only been received on incorrect nodes. Operators that are confident of topology parity across all nodes may
|
||||
disable rejection to maximize availability. See CASSANDRA-13704 for further details. Transactional Cluster
|
||||
Metadata in version 5.1 will remove all such edge-cases, providing appropriate guarantees.
|
||||
|
||||
4.1.3
|
||||
=====
|
||||
|
|
|
|||
|
|
@ -741,6 +741,13 @@ public class Config
|
|||
*/
|
||||
public ConsistencyLevel denylist_consistency_level = ConsistencyLevel.QUORUM;
|
||||
|
||||
/*
|
||||
* Toggles to turn on the logging or rejection of operations for token ranges that the node does not own,
|
||||
* or is not about to acquire.
|
||||
*/
|
||||
public volatile boolean log_out_of_token_range_requests = true;
|
||||
public volatile boolean reject_out_of_token_range_requests = true;
|
||||
|
||||
/**
|
||||
* The intial capacity for creating RangeTombstoneList.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4516,4 +4516,24 @@ public class DatabaseDescriptor
|
|||
{
|
||||
conf.cql_start_time = value;
|
||||
}
|
||||
|
||||
public static boolean getLogOutOfTokenRangeRequests()
|
||||
{
|
||||
return conf.log_out_of_token_range_requests;
|
||||
}
|
||||
|
||||
public static void setLogOutOfTokenRangeRequests(boolean enabled)
|
||||
{
|
||||
conf.log_out_of_token_range_requests = enabled;
|
||||
}
|
||||
|
||||
public static boolean getRejectOutOfTokenRangeRequests()
|
||||
{
|
||||
return conf.reject_out_of_token_range_requests;
|
||||
}
|
||||
|
||||
public static void setRejectOutOfTokenRangeRequests(boolean enabled)
|
||||
{
|
||||
conf.reject_out_of_token_range_requests = enabled;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
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.service.StorageService;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
|
||||
public abstract class AbstractMutationVerbHandler<T extends IMutation> implements IVerbHandler<T>
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(AbstractMutationVerbHandler.class);
|
||||
private static final String logMessageTemplate = "Receiving mutation(s) for token(s) neither owned nor pending. Example: from {} for token {} in keyspace {}";
|
||||
|
||||
@Override
|
||||
public void doVerb(Message<T> message) throws IOException
|
||||
{
|
||||
processMessage(message, message.from());
|
||||
}
|
||||
|
||||
public void processMessage(Message<T> message, InetAddressAndPort respondTo)
|
||||
{
|
||||
boolean outOfRangeTokenLogging = DatabaseDescriptor.getLogOutOfTokenRangeRequests();
|
||||
boolean outOfRangeTokenRejection = DatabaseDescriptor.getRejectOutOfTokenRangeRequests();
|
||||
|
||||
DecoratedKey key = message.payload.key();
|
||||
boolean isOutOfRangeMutation = isOutOfRangeMutation(message.payload.getKeyspaceName(), key);
|
||||
if (isOutOfRangeMutation)
|
||||
{
|
||||
StorageService.instance.incOutOfRangeOperationCount();
|
||||
Keyspace.open(message.payload.getKeyspaceName()).metric.outOfRangeTokenWrites.inc();
|
||||
|
||||
if (outOfRangeTokenLogging)
|
||||
NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, logMessageTemplate,
|
||||
respondTo, key.getToken(), message.payload.getKeyspaceName());
|
||||
}
|
||||
if (outOfRangeTokenRejection && isOutOfRangeMutation)
|
||||
sendFailureResponse(message, respondTo);
|
||||
else
|
||||
applyMutation(message, respondTo);
|
||||
}
|
||||
|
||||
abstract void applyMutation(Message<T> message, InetAddressAndPort respondToAddress);
|
||||
|
||||
private void sendFailureResponse(Message<T> respondTo, InetAddressAndPort respondToAddress)
|
||||
{
|
||||
MessagingService.instance().send(respondTo.failureResponse(RequestFailureReason.UNKNOWN), respondToAddress);
|
||||
|
||||
}
|
||||
|
||||
private static boolean isOutOfRangeMutation(String keyspace, DecoratedKey key)
|
||||
{
|
||||
return !StorageService.instance.isEndpointValidForWrite(keyspace, key.getToken());
|
||||
}
|
||||
}
|
||||
|
|
@ -21,19 +21,20 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.net.IVerbHandler;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.service.StorageProxy;
|
||||
import org.apache.cassandra.transport.Dispatcher;
|
||||
|
||||
public class CounterMutationVerbHandler implements IVerbHandler<CounterMutation>
|
||||
public class CounterMutationVerbHandler extends AbstractMutationVerbHandler<CounterMutation>
|
||||
{
|
||||
public static final CounterMutationVerbHandler instance = new CounterMutationVerbHandler();
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CounterMutationVerbHandler.class);
|
||||
|
||||
public void doVerb(final Message<CounterMutation> message)
|
||||
@Override
|
||||
protected void applyMutation(final Message<CounterMutation> message, InetAddressAndPort respondToAddress)
|
||||
{
|
||||
final CounterMutation cm = message.payload;
|
||||
logger.trace("Applying forwarded {}", cm);
|
||||
|
|
@ -48,7 +49,7 @@ public class CounterMutationVerbHandler implements IVerbHandler<CounterMutation>
|
|||
// it's own in that case.
|
||||
StorageProxy.applyCounterMutationOnLeader(cm,
|
||||
localDataCenter,
|
||||
() -> MessagingService.instance().send(message.emptyResponse(), message.from()),
|
||||
() -> MessagingService.instance().send(message.emptyResponse(), respondToAddress),
|
||||
Dispatcher.RequestTime.forImmediateExecution());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import static java.util.concurrent.TimeUnit.NANOSECONDS;
|
|||
import static org.apache.cassandra.db.commitlog.CommitLogSegment.ENTRY_OVERHEAD_SIZE;
|
||||
import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
|
||||
|
||||
public class MutationVerbHandler implements IVerbHandler<Mutation>
|
||||
public class MutationVerbHandler extends AbstractMutationVerbHandler<Mutation>
|
||||
{
|
||||
public static final MutationVerbHandler instance = new MutationVerbHandler();
|
||||
|
||||
|
|
@ -41,6 +41,7 @@ public class MutationVerbHandler implements IVerbHandler<Mutation>
|
|||
Tracing.trace("Payload application resulted in WriteTimeout, not replying");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doVerb(Message<Mutation> message)
|
||||
{
|
||||
if (approxTime.now() > message.expiresAtNanos())
|
||||
|
|
@ -60,7 +61,7 @@ public class MutationVerbHandler implements IVerbHandler<Mutation>
|
|||
InetAddressAndPort respondToAddress = message.respondTo();
|
||||
try
|
||||
{
|
||||
message.payload.applyFuture().addCallback(o -> respond(message, respondToAddress), wto -> failed());
|
||||
processMessage(message, respondToAddress);
|
||||
}
|
||||
catch (WriteTimeoutException wto)
|
||||
{
|
||||
|
|
@ -68,6 +69,12 @@ public class MutationVerbHandler implements IVerbHandler<Mutation>
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyMutation(Message<Mutation> message, InetAddressAndPort respondToAddress)
|
||||
{
|
||||
message.payload.applyFuture().addCallback(o -> respond(message, respondToAddress), wto -> failed());
|
||||
}
|
||||
|
||||
private static void forwardToLocalNodes(Message<Mutation> originalMessage, ForwardingInfo forwardTo)
|
||||
{
|
||||
Message.Builder<Mutation> builder =
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR
|
|||
|
||||
protected final DataRange dataRange;
|
||||
|
||||
private PartitionRangeReadCommand(boolean isDigest,
|
||||
@VisibleForTesting
|
||||
protected PartitionRangeReadCommand(boolean isDigest,
|
||||
int digestVersion,
|
||||
boolean acceptsTransient,
|
||||
TableMetadata metadata,
|
||||
|
|
|
|||
|
|
@ -168,6 +168,16 @@ public abstract class ReadCommand extends AbstractReadQuery
|
|||
|
||||
public abstract boolean isLimitedToOnePartition();
|
||||
|
||||
/**
|
||||
* Whether this command is a single partition read
|
||||
*
|
||||
* @return true if the command is a single partition read, false otherwise
|
||||
*/
|
||||
public boolean isSinglePartitionRead()
|
||||
{
|
||||
return kind == Kind.SINGLE_PARTITION;
|
||||
}
|
||||
|
||||
public abstract boolean isRangeRequest();
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
*/
|
||||
package org.apache.cassandra.db;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -30,6 +32,7 @@ import org.apache.cassandra.net.Message;
|
|||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.tracing.Tracing;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.NANOSECONDS;
|
||||
|
||||
|
|
@ -38,6 +41,8 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
|
|||
public static final ReadCommandVerbHandler instance = new ReadCommandVerbHandler();
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ReadCommandVerbHandler.class);
|
||||
private static final String logMessageTemplate = "Receiving read(s) for token(s) neither owned nor pending. Example: from {} for token {} in {}.{}";
|
||||
private static final String exceptionMessageTemplate = "Exception thrown checking if token {} outside valid range for keyspace {} - permitting";
|
||||
|
||||
public void doVerb(Message<ReadCommand> message)
|
||||
{
|
||||
|
|
@ -47,6 +52,30 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
|
|||
}
|
||||
|
||||
ReadCommand command = message.payload;
|
||||
|
||||
// no out of token range checking for partition range reads yet
|
||||
if (command.isSinglePartitionRead())
|
||||
{
|
||||
boolean outOfRangeTokenLogging = DatabaseDescriptor.getLogOutOfTokenRangeRequests();
|
||||
boolean outOfRangeTokenRejection = DatabaseDescriptor.getRejectOutOfTokenRangeRequests();
|
||||
|
||||
DecoratedKey key = ((SinglePartitionReadCommand)command).partitionKey();
|
||||
if (!command.metadata().isVirtual() && isOutOfRangeRead(command.metadata().keyspace, key))
|
||||
{
|
||||
StorageService.instance.incOutOfRangeOperationCount();
|
||||
Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc();
|
||||
|
||||
// Log at most 1 message per second
|
||||
if (outOfRangeTokenLogging)
|
||||
NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, logMessageTemplate,
|
||||
message.from(), key.getToken(), command.metadata().keyspace, command.metadata().name);
|
||||
|
||||
if (outOfRangeTokenRejection)
|
||||
// no need to respond, just drop the request
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
validateTransientStatus(message);
|
||||
MessageParams.reset();
|
||||
|
||||
|
|
@ -124,4 +153,20 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
|
|||
this));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOutOfRangeRead(String keyspace, DecoratedKey key)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Keyspace.open(keyspace)
|
||||
.getReplicationStrategy()
|
||||
.getNaturalReplicas(key)
|
||||
.selfIfPresent() == null;
|
||||
}
|
||||
catch (Throwable tr)
|
||||
{
|
||||
NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, exceptionMessageTemplate, keyspace, key.getKey(), tr);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,17 +17,28 @@
|
|||
*/
|
||||
package org.apache.cassandra.db;
|
||||
|
||||
import org.apache.cassandra.net.IVerbHandler;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
|
||||
public class ReadRepairVerbHandler implements IVerbHandler<Mutation>
|
||||
public class ReadRepairVerbHandler extends AbstractMutationVerbHandler<Mutation>
|
||||
{
|
||||
public static final ReadRepairVerbHandler instance = new ReadRepairVerbHandler();
|
||||
|
||||
public void doVerb(Message<Mutation> message)
|
||||
@Override
|
||||
public void doVerb(Message<Mutation> message) throws IOException
|
||||
{
|
||||
// This method exists as python dtest relies on byte-code rewriting via Byteman, so requires this
|
||||
// class defines a "doVerb" method for some tests.
|
||||
super.doVerb(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
void applyMutation(Message<Mutation> message, InetAddressAndPort respondToAddress)
|
||||
{
|
||||
message.payload.apply();
|
||||
MessagingService.instance().send(message.emptyResponse(), message.from());
|
||||
MessagingService.instance().send(message.emptyResponse(), respondToAddress);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@ public class CassandraCompressedStreamReader extends CassandraStreamReader
|
|||
try (CompressedInputStream cis = new CompressedInputStream(inputPlus, compressionInfo, ChecksumType.CRC32, cfs::getCrcCheckChance))
|
||||
{
|
||||
TrackedDataInputPlus in = new TrackedDataInputPlus(cis);
|
||||
deserializer = new StreamDeserializer(cfs.metadata(), in, inputVersion, getHeader(cfs.metadata()));
|
||||
writer = createWriter(cfs, totalSize, repairedAt, pendingRepair, format);
|
||||
deserializer = new StreamDeserializer(cfs.metadata(), in, inputVersion, getHeader(cfs.metadata()), session, writer);
|
||||
String filename = writer.getFilename();
|
||||
String sectionName = filename + '-' + fileSeqNum;
|
||||
int sectionIdx = 0;
|
||||
|
|
|
|||
|
|
@ -20,12 +20,16 @@ package org.apache.cassandra.db.streaming;
|
|||
import java.io.IOError;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.UnmodifiableIterator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||
import org.apache.cassandra.db.DecoratedKey;
|
||||
import org.apache.cassandra.db.DeletionTime;
|
||||
|
|
@ -38,6 +42,8 @@ import org.apache.cassandra.db.rows.EncodingStats;
|
|||
import org.apache.cassandra.db.rows.Row;
|
||||
import org.apache.cassandra.db.rows.Unfiltered;
|
||||
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.exceptions.UnknownColumnException;
|
||||
import org.apache.cassandra.io.sstable.SSTableMultiWriter;
|
||||
import org.apache.cassandra.io.sstable.SSTableSimpleIterator;
|
||||
|
|
@ -47,15 +53,19 @@ import org.apache.cassandra.io.sstable.format.SSTableReader;
|
|||
import org.apache.cassandra.io.sstable.format.Version;
|
||||
import org.apache.cassandra.io.util.DataInputPlus;
|
||||
import org.apache.cassandra.io.util.TrackedDataInputPlus;
|
||||
import org.apache.cassandra.metrics.StorageMetrics;
|
||||
import org.apache.cassandra.schema.TableId;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.streaming.ProgressInfo;
|
||||
import org.apache.cassandra.streaming.StreamReceivedOutOfTokenRangeException;
|
||||
import org.apache.cassandra.streaming.StreamReceiver;
|
||||
import org.apache.cassandra.streaming.StreamSession;
|
||||
import org.apache.cassandra.streaming.compress.StreamCompressionInputStream;
|
||||
import org.apache.cassandra.streaming.messages.StreamMessageHeader;
|
||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
import org.apache.cassandra.utils.TimeUUID;
|
||||
|
||||
import static org.apache.cassandra.net.MessagingService.current_version;
|
||||
|
|
@ -66,6 +76,7 @@ import static org.apache.cassandra.net.MessagingService.current_version;
|
|||
public class CassandraStreamReader implements IStreamReader
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(CassandraStreamReader.class);
|
||||
private static final String logMessageTemplate = "[Stream #{}] Received streamed SSTable {} from {} containing key(s) outside valid ranges {}. Example: {}";
|
||||
protected final TableId tableId;
|
||||
protected final long estimatedKeys;
|
||||
protected final Collection<SSTableReader.PartitionPositionBounds> sections;
|
||||
|
|
@ -124,8 +135,8 @@ public class CassandraStreamReader implements IStreamReader
|
|||
try (StreamCompressionInputStream streamCompressionInputStream = new StreamCompressionInputStream(inputPlus, current_version))
|
||||
{
|
||||
TrackedDataInputPlus in = new TrackedDataInputPlus(streamCompressionInputStream);
|
||||
deserializer = new StreamDeserializer(cfs.metadata(), in, inputVersion, getHeader(cfs.metadata()));
|
||||
writer = createWriter(cfs, totalSize, repairedAt, pendingRepair, format);
|
||||
deserializer = getDeserializer(cfs.metadata(), in, inputVersion, session, writer);
|
||||
String sequenceName = writer.getFilename() + '-' + fileSeqNum;
|
||||
long lastBytesRead = 0;
|
||||
while (in.getBytesRead() < totalSize)
|
||||
|
|
@ -152,6 +163,15 @@ public class CassandraStreamReader implements IStreamReader
|
|||
}
|
||||
}
|
||||
|
||||
protected StreamDeserializer getDeserializer(TableMetadata metadata,
|
||||
TrackedDataInputPlus in,
|
||||
Version inputVersion,
|
||||
StreamSession session,
|
||||
SSTableMultiWriter writer) throws IOException
|
||||
{
|
||||
return new StreamDeserializer(metadata, in, inputVersion, getHeader(metadata), session, writer);
|
||||
}
|
||||
|
||||
protected SerializationHeader getHeader(TableMetadata metadata) throws UnknownColumnException
|
||||
{
|
||||
return header != null? header.toHeader(metadata) : null; //pre-3.0 sstable have no SerializationHeader
|
||||
|
|
@ -192,27 +212,57 @@ public class CassandraStreamReader implements IStreamReader
|
|||
private final SerializationHeader header;
|
||||
private final DeserializationHelper helper;
|
||||
|
||||
private DecoratedKey key;
|
||||
private DeletionTime partitionLevelDeletion;
|
||||
private SSTableSimpleIterator iterator;
|
||||
private Row staticRow;
|
||||
private final List<Range<Token>> ownedRanges;
|
||||
private final boolean outOfRangeTokenLogging;
|
||||
private final boolean outOfRangeTokenRejection;
|
||||
private final StreamSession session;
|
||||
private final SSTableMultiWriter writer;
|
||||
|
||||
private int lastCheckedRangeIndex;
|
||||
protected DecoratedKey key;
|
||||
protected DeletionTime partitionLevelDeletion;
|
||||
protected SSTableSimpleIterator iterator;
|
||||
protected Row staticRow;
|
||||
private IOException exception;
|
||||
|
||||
public StreamDeserializer(TableMetadata metadata, DataInputPlus in, Version version, SerializationHeader header) throws IOException
|
||||
public StreamDeserializer(TableMetadata metadata, DataInputPlus in, Version version, SerializationHeader header, StreamSession session, SSTableMultiWriter writer) throws IOException
|
||||
{
|
||||
this.metadata = metadata;
|
||||
this.in = in;
|
||||
this.helper = new DeserializationHelper(metadata, version.correspondingMessagingVersion(), DeserializationHelper.Flag.PRESERVE_SIZE);
|
||||
this.header = header;
|
||||
this.session = session;
|
||||
this.writer = writer;
|
||||
|
||||
ownedRanges = Range.normalize(StorageService.instance.getLocalAndPendingRanges(metadata.keyspace));
|
||||
lastCheckedRangeIndex = 0;
|
||||
outOfRangeTokenLogging = DatabaseDescriptor.getLogOutOfTokenRangeRequests();
|
||||
outOfRangeTokenRejection = DatabaseDescriptor.getRejectOutOfTokenRangeRequests();
|
||||
}
|
||||
|
||||
public StreamDeserializer newPartition() throws IOException
|
||||
public UnfilteredRowIterator newPartition() throws IOException
|
||||
{
|
||||
readKey();
|
||||
readPartition();
|
||||
return this;
|
||||
}
|
||||
|
||||
protected void readKey() throws IOException
|
||||
{
|
||||
key = metadata.partitioner.decorateKey(ByteBufferUtil.readWithShortLength(in));
|
||||
|
||||
lastCheckedRangeIndex = verifyKeyInOwnedRanges(key,
|
||||
ownedRanges,
|
||||
lastCheckedRangeIndex,
|
||||
outOfRangeTokenLogging,
|
||||
outOfRangeTokenRejection);
|
||||
}
|
||||
|
||||
protected void readPartition() throws IOException
|
||||
{
|
||||
partitionLevelDeletion = DeletionTime.serializer.deserialize(in);
|
||||
iterator = SSTableSimpleIterator.create(metadata, in, header, helper, partitionLevelDeletion);
|
||||
staticRow = iterator.readStaticRow();
|
||||
return this;
|
||||
}
|
||||
|
||||
public TableMetadata metadata()
|
||||
|
|
@ -293,5 +343,35 @@ public class CassandraStreamReader implements IStreamReader
|
|||
public void close()
|
||||
{
|
||||
}
|
||||
|
||||
private int verifyKeyInOwnedRanges(final DecoratedKey key,
|
||||
List<Range<Token>> ownedRanges,
|
||||
int lastCheckedRangeIndex,
|
||||
boolean outOfRangeTokenLogging,
|
||||
boolean outOfRangeTokenRejection)
|
||||
{
|
||||
if (lastCheckedRangeIndex < ownedRanges.size())
|
||||
{
|
||||
ListIterator<Range<Token>> rangesToCheck = ownedRanges.listIterator(lastCheckedRangeIndex);
|
||||
while (rangesToCheck.hasNext())
|
||||
{
|
||||
Range<Token> range = rangesToCheck.next();
|
||||
if (range.contains(key.getToken()))
|
||||
return lastCheckedRangeIndex;
|
||||
|
||||
lastCheckedRangeIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
StorageMetrics.totalOpsForInvalidToken.inc();
|
||||
|
||||
if (outOfRangeTokenLogging)
|
||||
NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, logMessageTemplate, session.planId(), writer.getFilename(), session.peer, ownedRanges, key);
|
||||
|
||||
if (outOfRangeTokenRejection)
|
||||
throw new StreamReceivedOutOfTokenRangeException(ownedRanges, key, writer.getFilename());
|
||||
|
||||
return lastCheckedRangeIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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.dht;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.metrics.StorageMetrics;
|
||||
|
||||
public final class OwnedRanges
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(OwnedRanges.class);
|
||||
|
||||
private static final Comparator<Range<Token>> rangeComparator = Comparator.comparing((Range<Token> r) -> r.left).thenComparing(r -> r.right);
|
||||
|
||||
// the set of token ranges that this node is a replica for
|
||||
private final List<Range<Token>> ownedRanges;
|
||||
|
||||
public OwnedRanges(Collection<Range<Token>> ownedRanges)
|
||||
{
|
||||
this.ownedRanges = Range.normalize(ownedRanges);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that all ranges in a requested set are contained by those in the owned set. Used in several contexts, such
|
||||
* as validating StreamRequests in StreamSession & PrepareMessage and ValidationRequest in RepairMessageVerbHandler.
|
||||
* In those callers, we want to verify that the token ranges specified in some request from a peer are not outside
|
||||
* the ranges owned by the local node. There are 2 levels of response if invalid ranges are detected, controlled
|
||||
* by options in Config; logging the event and rejecting the request and either/neither/both of these options may be
|
||||
* enabled. If neither are enabled, we short ciruit and immediately return success without any further processing.
|
||||
* If either option is enabled, and we do detect unowned ranges in the request, we increment a metric then take further
|
||||
* action depending on the config.
|
||||
*
|
||||
* @param requestedRanges the set of token ranges contained in a request from a peer
|
||||
* @param requestId an identifier for the peer request, to be used in logging (e.g. Stream or Repair Session #)
|
||||
* @param requestType description of the request type, to be used in logging (e.g. "prepare request" or "validation")
|
||||
* @param from the originator of the request
|
||||
*
|
||||
* @return true if the request should be accepted (either because no checking was performed, invalid ranges were
|
||||
* identified but only the logging action is enabled, or because all request ranges were valid).
|
||||
* Otherwise, returns false to indicate the request should be rejected.
|
||||
*/
|
||||
public boolean validateRangeRequest(Collection<Range<Token>> requestedRanges, String requestId, String requestType, InetAddressAndPort from)
|
||||
{
|
||||
boolean outOfRangeTokenLogging = DatabaseDescriptor.getLogOutOfTokenRangeRequests();
|
||||
boolean outOfRangeTokenRejection = DatabaseDescriptor.getRejectOutOfTokenRangeRequests();
|
||||
|
||||
Collection<Range<Token>> unownedRanges = testRanges(requestedRanges);
|
||||
|
||||
if (!unownedRanges.isEmpty())
|
||||
{
|
||||
StorageMetrics.totalOpsForInvalidToken.inc();
|
||||
|
||||
if (outOfRangeTokenLogging)
|
||||
{
|
||||
logger.warn("[{}] Received {} from {} containing ranges {} outside valid ranges {}",
|
||||
requestId, requestType, from, unownedRanges, ownedRanges);
|
||||
}
|
||||
}
|
||||
|
||||
return !outOfRangeTokenRejection || unownedRanges.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a collection of ranges and returns ranges from that collection that are not covered by this node's owned ranges.
|
||||
* <p>
|
||||
* This normalizes the range collections internally, so:
|
||||
* a) be cautious about using this in any hot path
|
||||
* b) any returned ranges may not be identical to those present. That is, the returned values are post-normalization.
|
||||
* <p>
|
||||
* e.g. Given two collections:
|
||||
* { (0, 100], (100, 200] }
|
||||
* { (90, 100], (100, 110], (110, 300] }
|
||||
* the normalized forms are:
|
||||
* { (0, 200] }
|
||||
* { (90, 300] }
|
||||
* and so the return value would be:
|
||||
* { (90, 300] }
|
||||
* which is equivalent, but not strictly equal to any member of the original supplied collection.
|
||||
*
|
||||
* @param testedRanges collection of candidate ranges to be checked
|
||||
* @return the ranges in testedRanges which are not covered by the owned ranges
|
||||
*/
|
||||
@VisibleForTesting
|
||||
Collection<Range<Token>> testRanges(final Collection<Range<Token>> testedRanges)
|
||||
{
|
||||
if (ownedRanges.isEmpty())
|
||||
return testedRanges;
|
||||
|
||||
// now normalize the second and check coverage of its members in the normalized first collection
|
||||
return Range.normalize(testedRanges).stream().filter(requested ->
|
||||
{
|
||||
// Find the point at which the target range would insert into the superset
|
||||
int index = Collections.binarySearch(ownedRanges, requested, rangeComparator);
|
||||
|
||||
// an index >= 0 means an exact match was found, so we can definitely accept this range
|
||||
if (index >= 0)
|
||||
return false;
|
||||
|
||||
// convert to an insertion point in the superset
|
||||
index = Math.abs(index) - 1;
|
||||
|
||||
// target sorts before the last list item, so we only need to check that one
|
||||
if (index >= ownedRanges.size())
|
||||
return !ownedRanges.get(index - 1).contains(requested);
|
||||
|
||||
// target sorts before the first list item, so we only need to check that one
|
||||
if (index == 0)
|
||||
return !ownedRanges.get(index).contains(requested);
|
||||
|
||||
// otherwise, check if the range on either side of the insertion point wholly contains the target
|
||||
return !(ownedRanges.get(index - 1).contains(requested) || ownedRanges.get(index).contains(requested));
|
||||
}).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
|
@ -19,10 +19,12 @@
|
|||
package org.apache.cassandra.hints;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.partitions.PartitionUpdate;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.net.IVerbHandler;
|
||||
|
|
@ -31,10 +33,11 @@ import org.apache.cassandra.net.MessagingService;
|
|||
import org.apache.cassandra.serializers.MarshalException;
|
||||
import org.apache.cassandra.service.StorageProxy;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
|
||||
/**
|
||||
* Verb handler used both for hint dispatch and streaming.
|
||||
*
|
||||
* <p>
|
||||
* With the non-sstable format, we cannot just stream hint sstables on node decommission. So sometimes, at decommission
|
||||
* time, we might have to stream hints to a non-owning host (say, if the owning host B is down during decommission of host A).
|
||||
* In that case the handler just stores the received hint in its local hint store.
|
||||
|
|
@ -89,6 +92,22 @@ public final class HintVerbHandler implements IVerbHandler<HintMessage>
|
|||
// the topology has changed, and we are no longer a replica of the mutation - since we don't know which node(s)
|
||||
// it has been handed over to, re-address the hint to all replicas; see CASSANDRA-5902.
|
||||
HintsService.instance.writeForAllReplicas(hint);
|
||||
|
||||
HintsService.instance.metrics.incrHintsReceivedForUnownedRanges();
|
||||
if (DatabaseDescriptor.getLogOutOfTokenRangeRequests())
|
||||
{
|
||||
// Log at most 1 message per second
|
||||
NoSpamLogger.log(logger,
|
||||
NoSpamLogger.Level.WARN,
|
||||
1,
|
||||
TimeUnit.SECONDS,
|
||||
"Receiving hint(s) for token(s) neither owned nor pending. Example: from {} for token {} in {}.{}",
|
||||
message.from(),
|
||||
hint.mutation.key().getToken(),
|
||||
hint.mutation.getKeyspaceName(),
|
||||
hint.mutation.getPartitionUpdates().iterator().next().metadata().name);
|
||||
}
|
||||
|
||||
respond(message);
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -21,8 +21,16 @@ import java.io.IOException;
|
|||
import java.io.IOError;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.apache.cassandra.db.*;
|
||||
import org.apache.cassandra.db.rows.*;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import org.apache.cassandra.db.DeletionTime;
|
||||
import org.apache.cassandra.db.SerializationHeader;
|
||||
import org.apache.cassandra.db.rows.BTreeRow;
|
||||
import org.apache.cassandra.db.rows.DeserializationHelper;
|
||||
import org.apache.cassandra.db.rows.Row;
|
||||
import org.apache.cassandra.db.rows.Rows;
|
||||
import org.apache.cassandra.db.rows.Unfiltered;
|
||||
import org.apache.cassandra.db.rows.UnfilteredSerializer;
|
||||
import org.apache.cassandra.io.util.DataInputPlus;
|
||||
import org.apache.cassandra.io.util.FileDataInput;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
|
|
@ -30,7 +38,7 @@ import org.apache.cassandra.utils.AbstractIterator;
|
|||
|
||||
/**
|
||||
* Utility class to handle deserializing atom from sstables.
|
||||
*
|
||||
* <p>
|
||||
* Note that this is not a full fledged UnfilteredRowIterator. It's also not closeable, it is always
|
||||
* the job of the user to close the underlying ressources.
|
||||
*/
|
||||
|
|
@ -125,4 +133,28 @@ public abstract class SSTableSimpleIterator extends AbstractIterator<Unfiltered>
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static class EmptySSTableSimpleIterator extends SSTableSimpleIterator
|
||||
{
|
||||
public EmptySSTableSimpleIterator(TableMetadata metadata)
|
||||
{
|
||||
super(metadata, null, null);
|
||||
}
|
||||
|
||||
public Row readStaticRow()
|
||||
{
|
||||
return Rows.EMPTY_STATIC_ROW;
|
||||
}
|
||||
|
||||
protected Unfiltered computeNext()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean hasNext()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,11 +107,19 @@ public abstract class AbstractReplicationStrategy
|
|||
return endpoints;
|
||||
}
|
||||
|
||||
public Replica getLocalReplicaFor(RingPosition<?> searchPosition)
|
||||
/**
|
||||
* Check if the token is in a naturally replicated range or pending range
|
||||
* @param token the position to check
|
||||
* @return true if the token is in a natural or pending locally replicationed range, false otherwise
|
||||
*/
|
||||
public boolean isTokenInLocalNaturalOrPendingRange(Token token)
|
||||
{
|
||||
return getNaturalReplicas(searchPosition)
|
||||
.byEndpoint()
|
||||
.get(FBUtilities.getBroadcastAddressAndPort());
|
||||
return getLocalReplicaFor(token) != null || tokenMetadata.isTokenInLocalPendingRange(keyspaceName, token);
|
||||
}
|
||||
|
||||
public Replica getLocalReplicaFor(Token searchPosition)
|
||||
{
|
||||
return getNaturalReplicas(searchPosition).byEndpoint().get(FBUtilities.getBroadcastAddressAndPort());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -156,6 +156,12 @@ public class EndpointsForToken extends Endpoints<EndpointsForToken>
|
|||
return builder(token, replicas.size()).addAll(replicas).build();
|
||||
}
|
||||
|
||||
public static EndpointsForToken copyOf(Token token, Iterable<Replica> replicas)
|
||||
{
|
||||
if (!replicas.iterator().hasNext()) return empty(token);
|
||||
return builder(token).addAll(replicas).build();
|
||||
}
|
||||
|
||||
public static EndpointsForToken natural(Keyspace keyspace, Token token)
|
||||
{
|
||||
return keyspace.getReplicationStrategy().getNaturalReplicasForToken(token);
|
||||
|
|
|
|||
|
|
@ -20,13 +20,18 @@
|
|||
*/
|
||||
package org.apache.cassandra.locator;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.google.common.collect.Iterators;
|
||||
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict;
|
||||
|
||||
import java.util.*;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.LINE_SEPARATOR;
|
||||
|
||||
|
|
@ -149,6 +154,45 @@ public class PendingRangeMaps implements Iterable<Map.Entry<Range<Token>, Endpoi
|
|||
}
|
||||
}
|
||||
|
||||
public boolean isTokenInLocalPendingRange(Token token)
|
||||
{
|
||||
Range<Token> searchRange = new Range<>(token, token);
|
||||
InetAddressAndPort self = FBUtilities.getBroadcastAddressAndPort();
|
||||
|
||||
// search for non-wrap-around maps
|
||||
NavigableMap<Range<Token>, EndpointsForRange.Builder> ascendingTailMap = ascendingMap.tailMap(searchRange, true);
|
||||
NavigableMap<Range<Token>, EndpointsForRange.Builder> descendingTailMap = descendingMap.tailMap(searchRange, false);
|
||||
|
||||
boolean ascMapSizeLTDescMapSize = ascendingTailMap.size() < descendingTailMap.size();
|
||||
NavigableMap<Range<Token>, EndpointsForRange.Builder> smallerMap = ascMapSizeLTDescMapSize ? ascendingTailMap : descendingTailMap;
|
||||
NavigableMap<Range<Token>, EndpointsForRange.Builder> biggerMap = ascMapSizeLTDescMapSize ? descendingTailMap : ascendingTailMap;
|
||||
|
||||
// find the intersection of two sets
|
||||
for (Range<Token> range : smallerMap.keySet())
|
||||
{
|
||||
EndpointsForRange.Builder replicas = biggerMap.get(range);
|
||||
if (replicas != null && replicas.contains(self))
|
||||
return true;
|
||||
}
|
||||
|
||||
// search for wrap-around sets
|
||||
ascendingTailMap = ascendingMapForWrapAround.tailMap(searchRange, true);
|
||||
descendingTailMap = descendingMapForWrapAround.tailMap(searchRange, false);
|
||||
|
||||
for (EndpointsForRange.Builder endpointsForRange : ascendingTailMap.values())
|
||||
{
|
||||
if (endpointsForRange.contains(self))
|
||||
return true;
|
||||
}
|
||||
for (EndpointsForRange.Builder endpointsForRange : descendingTailMap.values())
|
||||
{
|
||||
if (endpointsForRange.contains(self))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public EndpointsForToken pendingEndpointsFor(Token token)
|
||||
{
|
||||
EndpointsForToken.Builder replicas = EndpointsForToken.builder(token);
|
||||
|
|
|
|||
|
|
@ -839,6 +839,14 @@ public class TokenMetadata
|
|||
return builder.build();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setPendingRangesUnsafe(String keyspaceName, Multimap<Range<Token>, Replica> rangeMap)
|
||||
{
|
||||
PendingRangeMaps prm = new PendingRangeMaps();
|
||||
rangeMap.entries().forEach(entry -> prm.addPendingRange(entry.getKey(), entry.getValue()));
|
||||
pendingRanges.put(keyspaceName, prm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate pending ranges according to bootsrapping and leaving nodes. Reasoning is:
|
||||
*
|
||||
|
|
@ -1433,6 +1441,12 @@ public class TokenMetadata
|
|||
return partitioner.decorateKey(key);
|
||||
}
|
||||
|
||||
public boolean isTokenInLocalPendingRange(String keyspaceName, Token token)
|
||||
{
|
||||
PendingRangeMaps pending = pendingRanges.get(keyspaceName);
|
||||
return pending == null ? false : pending.isTokenInLocalPendingRange(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the assignment of racks and endpoints in each datacenter for all the "normal" endpoints
|
||||
* in this TokenMetadata. This allows faster calculation of endpoints in NetworkTopologyStrategy.
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ public class HintedHandoffMetrics
|
|||
.executor(ImmediateExecutor.INSTANCE)
|
||||
.build(address -> Metrics.counter(factory.createMetricName("Hints_created-" + address.toString().replace(':', '.'))));
|
||||
|
||||
/** Counter of hints received whose mutations are for keys outside the owned and pending ranges for this node */
|
||||
private final Counter hintsReceivedForUnownedRanges = Metrics.counter(factory.createMetricName("Hints_for_unowned_ranges"));
|
||||
|
||||
public void incrCreatedHints(InetAddressAndPort address)
|
||||
{
|
||||
createdHintCounts.get(address).inc();
|
||||
|
|
@ -62,6 +65,11 @@ public class HintedHandoffMetrics
|
|||
notStored.get(address).mark();
|
||||
}
|
||||
|
||||
public void incrHintsReceivedForUnownedRanges()
|
||||
{
|
||||
hintsReceivedForUnownedRanges.inc();
|
||||
}
|
||||
|
||||
public void log()
|
||||
{
|
||||
for (Entry<InetAddressAndPort, DifferencingCounter> entry : notStored.asMap().entrySet())
|
||||
|
|
|
|||
|
|
@ -125,6 +125,12 @@ public class KeyspaceMetrics
|
|||
public final Histogram bytesValidated;
|
||||
/** histogram over the number of partitions we have validated */
|
||||
public final Histogram partitionsValidated;
|
||||
/** Lifetime count of reads for keys outside the node's owned token ranges for this keyspace **/
|
||||
public final Counter outOfRangeTokenReads;
|
||||
/** Lifetime count of writes for keys outside the node's owned token ranges for this keyspace **/
|
||||
public final Counter outOfRangeTokenWrites;
|
||||
/** Lifetime count of paxos requests for keys outside the node's owned token ranges for this keyspace **/
|
||||
public final Counter outOfRangeTokenPaxosRequests;
|
||||
|
||||
/*
|
||||
* Metrics for inconsistencies detected between repaired data sets across replicas. These
|
||||
|
|
@ -265,6 +271,10 @@ public class KeyspaceMetrics
|
|||
rowIndexSizeWarnings = createKeyspaceMeter("RowIndexSizeWarnings");
|
||||
rowIndexSizeAborts = createKeyspaceMeter("RowIndexSizeAborts");
|
||||
rowIndexSize = createKeyspaceHistogram("RowIndexSize", false);
|
||||
|
||||
outOfRangeTokenReads = createKeyspaceCounter("ReadOutOfRangeToken");
|
||||
outOfRangeTokenWrites = createKeyspaceCounter("WriteOutOfRangeToken");
|
||||
outOfRangeTokenPaxosRequests = createKeyspaceCounter("PaxosOutOfRangeToken");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -33,4 +33,6 @@ public class StorageMetrics
|
|||
public static final Counter totalHintsInProgress = Metrics.counter(factory.createMetricName("TotalHintsInProgress"));
|
||||
public static final Counter totalHints = Metrics.counter(factory.createMetricName("TotalHints"));
|
||||
public static final Counter repairExceptions = Metrics.counter(factory.createMetricName("RepairExceptions"));
|
||||
public static final Counter totalOpsForInvalidToken = Metrics.counter(factory.createMetricName("TotalOpsForInvalidToken"));
|
||||
public static final Counter startupOpsForInvalidToken = Metrics.counter(factory.createMetricName("StartupOpsForInvalidToken"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,22 +17,39 @@
|
|||
*/
|
||||
package org.apache.cassandra.repair;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||
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.repair.messages.*;
|
||||
import org.apache.cassandra.repair.messages.CleanupMessage;
|
||||
import org.apache.cassandra.repair.messages.FailSession;
|
||||
import org.apache.cassandra.repair.messages.FinalizeCommit;
|
||||
import org.apache.cassandra.repair.messages.FinalizePromise;
|
||||
import org.apache.cassandra.repair.messages.FinalizePropose;
|
||||
import org.apache.cassandra.repair.messages.PrepareConsistentRequest;
|
||||
import org.apache.cassandra.repair.messages.PrepareConsistentResponse;
|
||||
import org.apache.cassandra.repair.messages.PrepareMessage;
|
||||
import org.apache.cassandra.repair.messages.RepairMessage;
|
||||
import org.apache.cassandra.repair.messages.StatusRequest;
|
||||
import org.apache.cassandra.repair.messages.StatusResponse;
|
||||
import org.apache.cassandra.repair.messages.SyncRequest;
|
||||
import org.apache.cassandra.repair.messages.ValidationRequest;
|
||||
import org.apache.cassandra.repair.messages.ValidationResponse;
|
||||
import org.apache.cassandra.repair.state.ParticipateState;
|
||||
import org.apache.cassandra.repair.state.ValidationState;
|
||||
import org.apache.cassandra.schema.TableId;
|
||||
import org.apache.cassandra.service.ActiveRepairService;
|
||||
import org.apache.cassandra.streaming.PreviewKind;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.utils.TimeUUID;
|
||||
|
||||
import static org.apache.cassandra.net.Verb.VALIDATION_RSP;
|
||||
|
|
@ -59,6 +76,7 @@ public class RepairMessageVerbHandler implements IVerbHandler<RepairMessage>
|
|||
return prs != null ? prs.previewKind : PreviewKind.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doVerb(final Message<RepairMessage> message)
|
||||
{
|
||||
// TODO add cancel/interrupt message
|
||||
|
|
@ -194,8 +212,15 @@ public class RepairMessageVerbHandler implements IVerbHandler<RepairMessage>
|
|||
}
|
||||
|
||||
Validator validator = new Validator(vState, validationRequest.nowInSec,
|
||||
isIncremental(desc.parentSessionId), previewKind);
|
||||
ValidationManager.instance.submitValidation(store, validator);
|
||||
isIncremental(desc.parentSessionId), previewKind(desc.parentSessionId));
|
||||
if (acceptMessage(validationRequest, message.from()))
|
||||
{
|
||||
ValidationManager.instance.submitValidation(store, validator);
|
||||
}
|
||||
else
|
||||
{
|
||||
validator.fail(new RepairOutOfTokenRangeException(validationRequest.desc.ranges));
|
||||
}
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
|
|
@ -303,4 +328,19 @@ public class RepairMessageVerbHandler implements IVerbHandler<RepairMessage>
|
|||
Message<?> reply = respondTo.failureResponse(RequestFailureReason.UNKNOWN);
|
||||
MessagingService.instance().send(reply, respondTo.from());
|
||||
}
|
||||
|
||||
private static boolean acceptMessage(final ValidationRequest validationRequest, final InetAddressAndPort from)
|
||||
{
|
||||
boolean outOfRangeTokenLogging = DatabaseDescriptor.getLogOutOfTokenRangeRequests();
|
||||
boolean outOfRangeTokenRejection = DatabaseDescriptor.getRejectOutOfTokenRangeRequests();
|
||||
|
||||
if (!outOfRangeTokenLogging && !outOfRangeTokenRejection)
|
||||
return true;
|
||||
|
||||
return StorageService.instance.getNormalizedLocalRanges(validationRequest.desc.keyspace)
|
||||
.validateRangeRequest(validationRequest.desc.ranges,
|
||||
"RepairSession #" + validationRequest.desc.parentSessionId,
|
||||
"validation request",
|
||||
from);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.repair;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
|
||||
public class RepairOutOfTokenRangeException extends RuntimeException
|
||||
{
|
||||
public RepairOutOfTokenRangeException(Collection<Range<Token>> ownedRanges)
|
||||
{
|
||||
super(String.format("Received repair outside of owned ranges %s", ownedRanges));
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +121,7 @@ import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
|
|||
import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
|
||||
import org.apache.cassandra.dht.BootStrapper;
|
||||
import org.apache.cassandra.dht.IPartitioner;
|
||||
import org.apache.cassandra.dht.OwnedRanges;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.RangeStreamer;
|
||||
import org.apache.cassandra.dht.RangeStreamer.FetchReplica;
|
||||
|
|
@ -305,6 +306,35 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
|
|||
|
||||
public static final StorageService instance = new StorageService();
|
||||
|
||||
private final java.util.function.Predicate<Keyspace> anyOutOfRangeOpsRecorded
|
||||
= keyspace -> keyspace.metric.outOfRangeTokenReads.getCount() > 0
|
||||
|| keyspace.metric.outOfRangeTokenWrites.getCount() > 0
|
||||
|| keyspace.metric.outOfRangeTokenPaxosRequests.getCount() > 0;
|
||||
|
||||
private long[] getOutOfRangeOperationCounts(Keyspace keyspace)
|
||||
{
|
||||
return new long[]
|
||||
{
|
||||
keyspace.metric.outOfRangeTokenReads.getCount(),
|
||||
keyspace.metric.outOfRangeTokenWrites.getCount(),
|
||||
keyspace.metric.outOfRangeTokenPaxosRequests.getCount()
|
||||
};
|
||||
}
|
||||
|
||||
public Map<String, long[]> getOutOfRangeOperationCounts()
|
||||
{
|
||||
return Schema.instance.getKeyspaces()
|
||||
.stream()
|
||||
.map(Keyspace::open)
|
||||
.filter(anyOutOfRangeOpsRecorded)
|
||||
.collect(Collectors.toMap(Keyspace::getName, this::getOutOfRangeOperationCounts));
|
||||
}
|
||||
|
||||
public void incOutOfRangeOperationCount()
|
||||
{
|
||||
(isStarting() ? StorageMetrics.startupOpsForInvalidToken : StorageMetrics.totalOpsForInvalidToken).inc();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public boolean isInShutdownHook()
|
||||
{
|
||||
|
|
@ -353,6 +383,11 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
|
|||
return ranges;
|
||||
}
|
||||
|
||||
public OwnedRanges getNormalizedLocalRanges(String keyspaceName)
|
||||
{
|
||||
return new OwnedRanges(getLocalReplicas(keyspaceName).ranges());
|
||||
}
|
||||
|
||||
public Collection<Range<Token>> getPrimaryRanges(String keyspace)
|
||||
{
|
||||
return getPrimaryRangesForEndpoint(keyspace, FBUtilities.getBroadcastAddressAndPort());
|
||||
|
|
@ -4843,6 +4878,12 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
|
|||
return tokenMetadata.partitioner.getToken(partitionKeyToBytes(keyspaceName, table, key)).toString();
|
||||
}
|
||||
|
||||
public boolean isEndpointValidForWrite(String keyspace, Token token)
|
||||
{
|
||||
AbstractReplicationStrategy replicationStrategy = Keyspace.open(keyspace).getReplicationStrategy();
|
||||
return replicationStrategy.isTokenInLocalNaturalOrPendingRange(token);
|
||||
}
|
||||
|
||||
public void setLoggingLevel(String classQualifier, String rawLevel) throws Exception
|
||||
{
|
||||
LoggingSupportFactory.getLoggingSupport().setLoggingLevel(classQualifier, rawLevel);
|
||||
|
|
@ -6500,6 +6541,36 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
|
|||
return DatabaseDescriptor.getNativeTransportRateLimitingEnabled();
|
||||
}
|
||||
|
||||
public boolean isOutOfTokenRangeRequestLoggingEnabled()
|
||||
{
|
||||
return DatabaseDescriptor.getLogOutOfTokenRangeRequests();
|
||||
}
|
||||
|
||||
public void setOutOfTokenRangeRequestLoggingEnabled(boolean enabled)
|
||||
{
|
||||
if (enabled)
|
||||
logger.info("Enabling logging of requests on tokens outside owned ranges");
|
||||
else
|
||||
logger.info("Disabling logging of requests on tokens outside owned ranges");
|
||||
|
||||
DatabaseDescriptor.setLogOutOfTokenRangeRequests(enabled);
|
||||
}
|
||||
|
||||
public boolean isOutOfTokenRangeRequestRejectionEnabled()
|
||||
{
|
||||
return DatabaseDescriptor.getRejectOutOfTokenRangeRequests();
|
||||
}
|
||||
|
||||
public void setOutOfTokenRangeRequestRejectionEnabled(boolean enabled)
|
||||
{
|
||||
if (enabled)
|
||||
logger.info("Enabling rejection of requests on tokens outside owned ranges");
|
||||
else
|
||||
logger.info("Disabling rejection of requests on tokens outside owned ranges");
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(enabled);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void shutdownServer()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1069,4 +1069,22 @@ public interface StorageServiceMBean extends NotificationEmitter
|
|||
|
||||
boolean getEnforceNativeDeadlineForHints();
|
||||
void setEnforceNativeDeadlineForHints(boolean value);
|
||||
|
||||
/**
|
||||
* Toggles to turn on the logging or rejection of operations for token ranges that the node does not own,
|
||||
* or is not about to acquire.
|
||||
*/
|
||||
boolean isOutOfTokenRangeRequestLoggingEnabled();
|
||||
void setOutOfTokenRangeRequestLoggingEnabled(boolean enabled);
|
||||
|
||||
boolean isOutOfTokenRangeRequestRejectionEnabled();
|
||||
void setOutOfTokenRangeRequestRejectionEnabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* Get the per-keyspace counts of operations that the node has received for tokens outside
|
||||
* its owned ranges. Represented as a {@code Map<String, long[]>}, keys are keyspace names and the
|
||||
* values are the counts for read, write and paxos ops respectivly.
|
||||
* e.g. keyspace_name -> [reads, writes, paxos].
|
||||
*/
|
||||
Map<String, long[]> getOutOfRangeOperationCounts();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,15 @@ import org.apache.cassandra.transport.Dispatcher;
|
|||
import org.apache.cassandra.utils.Clock;
|
||||
import org.apache.cassandra.utils.concurrent.CountDownLatch;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.ConsistencyLevel;
|
||||
import org.apache.cassandra.db.WriteType;
|
||||
import org.apache.cassandra.exceptions.RequestFailureReason;
|
||||
import org.apache.cassandra.exceptions.WriteTimeoutException;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.net.RequestCallback;
|
||||
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
|
||||
|
||||
|
|
@ -33,6 +38,7 @@ import static org.apache.cassandra.utils.concurrent.CountDownLatch.newCountDownL
|
|||
|
||||
public abstract class AbstractPaxosCallback<T> implements RequestCallback<T>
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(AbstractPaxosCallback.class);
|
||||
protected final CountDownLatch latch;
|
||||
protected final int targets;
|
||||
private final ConsistencyLevel consistency;
|
||||
|
|
@ -66,4 +72,16 @@ public abstract class AbstractPaxosCallback<T> implements RequestCallback<T>
|
|||
throw new UncheckedInterruptedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason)
|
||||
{
|
||||
logger.debug("Received paxos propose/prepare failure response from {} reason {}", from, failureReason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invokeOnFailure()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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.v1;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.DecoratedKey;
|
||||
import org.apache.cassandra.exceptions.RequestFailureReason;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.net.IVerbHandler;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.service.paxos.Commit;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
|
||||
public abstract class AbstractPaxosVerbHandler implements IVerbHandler<Commit>
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(AbstractPaxosVerbHandler.class);
|
||||
private static final String logMessageTemplate = "Receiving Paxos operation(s) for token(s) neither owned nor pending. Example: from {} for token {} in {}.{}";
|
||||
|
||||
@Override
|
||||
public void doVerb(Message<Commit> message)
|
||||
{
|
||||
boolean outOfRangeTokenLogging = DatabaseDescriptor.getLogOutOfTokenRangeRequests();
|
||||
boolean outOfRangeTokenRejection = DatabaseDescriptor.getRejectOutOfTokenRangeRequests();
|
||||
|
||||
Commit commit = message.payload;
|
||||
DecoratedKey key = commit.update.partitionKey();
|
||||
boolean isOutOfRangeCommit = isOutOfRangeCommit(commit.update.metadata().keyspace, key);
|
||||
if (isOutOfRangeCommit)
|
||||
{
|
||||
StorageService.instance.incOutOfRangeOperationCount();
|
||||
Keyspace.open(commit.update.metadata().keyspace).metric.outOfRangeTokenPaxosRequests.inc();
|
||||
|
||||
// Log at most 1 message per second
|
||||
if (outOfRangeTokenLogging)
|
||||
NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, logMessageTemplate,
|
||||
message.from(), key.getToken(), commit.update.metadata().keyspace, commit.update.metadata().name);
|
||||
|
||||
}
|
||||
if (outOfRangeTokenRejection && isOutOfRangeCommit)
|
||||
sendFailureResponse(message);
|
||||
else
|
||||
processMessage(message);
|
||||
}
|
||||
|
||||
abstract void processMessage(Message<Commit> message);
|
||||
|
||||
private static void sendFailureResponse(Message<?> respondTo)
|
||||
{
|
||||
Message<?> reply = respondTo.failureResponse(RequestFailureReason.UNKNOWN);
|
||||
MessagingService.instance().send(reply, respondTo.from());
|
||||
}
|
||||
|
||||
private static boolean isOutOfRangeCommit(String keyspace, DecoratedKey key)
|
||||
{
|
||||
return ! StorageService.instance.isEndpointValidForWrite(keyspace, key.getToken());
|
||||
}
|
||||
}
|
||||
|
|
@ -17,14 +17,13 @@
|
|||
*/
|
||||
|
||||
package org.apache.cassandra.service.paxos.v1;
|
||||
import org.apache.cassandra.net.IVerbHandler;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.service.paxos.Commit;
|
||||
import org.apache.cassandra.service.paxos.PaxosState;
|
||||
import org.apache.cassandra.service.paxos.PrepareResponse;
|
||||
|
||||
public class PrepareVerbHandler implements IVerbHandler<Commit>
|
||||
public class PrepareVerbHandler extends AbstractPaxosVerbHandler
|
||||
{
|
||||
public static PrepareVerbHandler instance = new PrepareVerbHandler();
|
||||
|
||||
|
|
@ -33,7 +32,8 @@ public class PrepareVerbHandler implements IVerbHandler<Commit>
|
|||
return PaxosState.legacyPrepare(toPrepare);
|
||||
}
|
||||
|
||||
public void doVerb(Message<Commit> message)
|
||||
@Override
|
||||
public void processMessage(Message<Commit> message)
|
||||
{
|
||||
Message<PrepareResponse> reply = message.responseWith(doPrepare(message.payload));
|
||||
MessagingService.instance().send(reply, message.from());
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import org.apache.cassandra.net.MessagingService;
|
|||
import org.apache.cassandra.service.paxos.Commit;
|
||||
import org.apache.cassandra.service.paxos.PaxosState;
|
||||
|
||||
public class ProposeVerbHandler implements IVerbHandler<Commit>
|
||||
public class ProposeVerbHandler extends AbstractPaxosVerbHandler implements IVerbHandler<Commit>
|
||||
{
|
||||
public static final ProposeVerbHandler instance = new ProposeVerbHandler();
|
||||
|
||||
|
|
@ -32,7 +32,8 @@ public class ProposeVerbHandler implements IVerbHandler<Commit>
|
|||
return PaxosState.legacyPropose(proposal);
|
||||
}
|
||||
|
||||
public void doVerb(Message<Commit> message)
|
||||
@Override
|
||||
void processMessage(Message<Commit> message)
|
||||
{
|
||||
Boolean response = doPropose(message.payload);
|
||||
Message<Boolean> reply = message.responseWith(response);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.streaming;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.apache.cassandra.db.DecoratedKey;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
|
||||
public class StreamReceivedOutOfTokenRangeException extends RuntimeException
|
||||
{
|
||||
private final Collection<Range<Token>> ownedRanges;
|
||||
private final DecoratedKey key;
|
||||
private final String filename;
|
||||
|
||||
public StreamReceivedOutOfTokenRangeException(Collection<Range<Token>> ownedRanges,
|
||||
DecoratedKey key,
|
||||
String filename)
|
||||
{
|
||||
this.ownedRanges = ownedRanges;
|
||||
this.key = key;
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public String getMessage()
|
||||
{
|
||||
return String.format("Received stream for sstable %s containing key %s outside of owned ranges %s ",
|
||||
filename,
|
||||
key,
|
||||
ownedRanges);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,8 @@ import java.util.Collection;
|
|||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
|
||||
import org.apache.cassandra.db.TypeSizes;
|
||||
import org.apache.cassandra.dht.IPartitioner;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
|
|
@ -61,6 +63,17 @@ public class StreamRequest
|
|||
this.columnFamilies.addAll(columnFamilies);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("keyspace", keyspace)
|
||||
.add("tables", columnFamilies)
|
||||
.add("full", full)
|
||||
.add("transientReplicas", transientReplicas)
|
||||
.omitNullValues()
|
||||
.toString();
|
||||
}
|
||||
|
||||
public static class StreamRequestSerializer implements IVersionedSerializer<StreamRequest>
|
||||
{
|
||||
public void serialize(StreamRequest request, DataOutputPlus out, int version) throws IOException
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.streaming;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class StreamRequestOutOfTokenRangeException extends RuntimeException
|
||||
{
|
||||
private final Collection<StreamRequest> requests;
|
||||
|
||||
public StreamRequestOutOfTokenRangeException(Collection<StreamRequest> requests)
|
||||
{
|
||||
this.requests = requests;
|
||||
}
|
||||
|
||||
public String getMessage()
|
||||
{
|
||||
return String.format("Received stream requests containing ranges outside of owned ranges: %s", requests);
|
||||
}
|
||||
}
|
||||
|
|
@ -34,36 +34,40 @@ import java.util.concurrent.TimeUnit;
|
|||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.util.concurrent.Future; //checkstyle: permit this import
|
||||
import org.apache.cassandra.concurrent.ScheduledExecutors;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.locator.RangesAtEndpoint;
|
||||
|
||||
import org.apache.cassandra.utils.TimeUUID;
|
||||
import org.apache.cassandra.utils.concurrent.FutureCombiner;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.db.lifecycle.TransactionAlreadyCompletedException;
|
||||
import org.apache.cassandra.dht.OwnedRanges;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.gms.*;
|
||||
import org.apache.cassandra.gms.EndpointState;
|
||||
import org.apache.cassandra.gms.IEndpointStateChangeSubscriber;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.locator.RangesAtEndpoint;
|
||||
import org.apache.cassandra.locator.Replica;
|
||||
import org.apache.cassandra.metrics.StreamingMetrics;
|
||||
import org.apache.cassandra.schema.TableId;
|
||||
import org.apache.cassandra.streaming.async.StreamingMultiplexedChannel;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.streaming.messages.*;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
import org.apache.cassandra.utils.TimeUUID;
|
||||
import org.apache.cassandra.utils.concurrent.FutureCombiner;
|
||||
|
||||
import static com.google.common.collect.Iterables.all;
|
||||
import static org.apache.cassandra.utils.Clock.Global.nanoTime;
|
||||
|
|
@ -101,7 +105,7 @@ import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort;
|
|||
*
|
||||
* 3. Streaming phase
|
||||
*
|
||||
* (a) The streaming phase is started at each node by calling {@link StreamSession#startStreamingFiles(boolean)}.
|
||||
* (a) The streaming phase is started at each node by calling {@link StreamSession#startStreamingFiles(PrepareDirection)}.
|
||||
* This will send, sequentially on each outbound streaming connection (see {@link StreamingMultiplexedChannel}),
|
||||
* an {@link OutgoingStreamMessage} for each stream in each of the {@link StreamTransferTask}.
|
||||
* Each {@link OutgoingStreamMessage} consists of a {@link StreamMessageHeader} that contains metadata about
|
||||
|
|
@ -378,7 +382,7 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
getPendingRepair(),
|
||||
getPreviewKind());
|
||||
|
||||
channel.sendControlMessage(message).sync();
|
||||
sendControlMessage(message).sync();
|
||||
onInitializationComplete();
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -658,7 +662,7 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
prepare.summaries.add(task.getSummary());
|
||||
}
|
||||
|
||||
channel.sendControlMessage(prepare).syncUninterruptibly();
|
||||
sendControlMessage(prepare).syncUninterruptibly();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -703,7 +707,7 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
if (channel.connected())
|
||||
{
|
||||
state(State.FAILED); // make sure subsequent error handling sees the session in a final state
|
||||
channel.sendControlMessage(new SessionFailedMessage()).awaitUninterruptibly();
|
||||
sendControlMessage(new SessionFailedMessage()).awaitUninterruptibly();
|
||||
}
|
||||
|
||||
return closeSession(State.FAILED);
|
||||
|
|
@ -757,10 +761,10 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
* Finish preparing the session. This method is blocking (memtables are flushed in {@link #addTransferRanges}),
|
||||
* so the logic should not execute on the main IO thread (read: netty event loop).
|
||||
*/
|
||||
private void prepareAsync(Collection<StreamRequest> requests, Collection<StreamSummary> summaries)
|
||||
@VisibleForTesting
|
||||
public void prepareAsync(Collection<StreamRequest> requests, Collection<StreamSummary> summaries)
|
||||
{
|
||||
for (StreamRequest request : requests)
|
||||
addTransferRanges(request.keyspace, RangesAtEndpoint.concat(request.full, request.transientReplicas), request.columnFamilies, true); // always flush on stream request
|
||||
processStreamRequests(requests);
|
||||
for (StreamSummary summary : summaries)
|
||||
prepareReceiving(summary);
|
||||
|
||||
|
|
@ -777,7 +781,7 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
// see CASSANDRA-17116
|
||||
if (isPreview())
|
||||
state(State.COMPLETE);
|
||||
channel.sendControlMessage(prepareSynAck).syncUninterruptibly();
|
||||
sendControlMessage(prepareSynAck).syncUninterruptibly();
|
||||
|
||||
if (isPreview())
|
||||
completePreview();
|
||||
|
|
@ -794,7 +798,7 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
|
||||
// only send the (final) ACK if we are expecting the peer to send this node (the initiator) some files
|
||||
if (!isPreview())
|
||||
channel.sendControlMessage(new PrepareAckMessage()).syncUninterruptibly();
|
||||
sendControlMessage(new PrepareAckMessage()).syncUninterruptibly();
|
||||
}
|
||||
|
||||
if (isPreview())
|
||||
|
|
@ -810,6 +814,32 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
startStreamingFiles(PrepareDirection.ACK);
|
||||
}
|
||||
|
||||
private void processStreamRequests(Collection<StreamRequest> requests)
|
||||
{
|
||||
List<StreamRequest> rejectedRequests = new ArrayList<>();
|
||||
|
||||
// group requests by keyspace
|
||||
Multimap<String, StreamRequest> requestsByKeyspace = ArrayListMultimap.create();
|
||||
requests.forEach(r -> requestsByKeyspace.put(r.keyspace, r));
|
||||
|
||||
requestsByKeyspace.asMap().forEach((ks, reqs) ->
|
||||
{
|
||||
OwnedRanges ownedRanges = StorageService.instance.getNormalizedLocalRanges(ks);
|
||||
|
||||
reqs.forEach(req ->
|
||||
{
|
||||
RangesAtEndpoint allRangesAtEndpoint = RangesAtEndpoint.concat(req.full, req.transientReplicas);
|
||||
if (ownedRanges.validateRangeRequest(allRangesAtEndpoint.ranges(), "Stream #" + planId(), "stream request", peer))
|
||||
addTransferRanges(req.keyspace, allRangesAtEndpoint, req.columnFamilies, true); // always flush on stream request
|
||||
else
|
||||
rejectedRequests.add(req);
|
||||
});
|
||||
});
|
||||
|
||||
if (!rejectedRequests.isEmpty())
|
||||
throw new StreamRequestOutOfTokenRangeException(rejectedRequests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call back after sending StreamMessageHeader.
|
||||
*
|
||||
|
|
@ -835,6 +865,12 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected Future<?> sendControlMessage(StreamMessage message)
|
||||
{
|
||||
return channel.sendControlMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call back after receiving a stream.
|
||||
*
|
||||
|
|
@ -851,7 +887,7 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
StreamingMetrics.totalIncomingBytes.inc(headerSize);
|
||||
metrics.incomingBytes.inc(headerSize);
|
||||
// send back file received message
|
||||
channel.sendControlMessage(new ReceivedMessage(message.header.tableId, message.header.sequenceNumber)).syncUninterruptibly();
|
||||
sendControlMessage(new ReceivedMessage(message.header.tableId, message.header.sequenceNumber)).syncUninterruptibly();
|
||||
StreamHook.instance.reportIncomingStream(message.header.tableId, message.stream, this, message.header.sequenceNumber);
|
||||
long receivedStartNanos = nanoTime();
|
||||
try
|
||||
|
|
@ -934,7 +970,7 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
// before sending the message (without closing the channel)
|
||||
// see CASSANDRA-17116
|
||||
state(State.COMPLETE);
|
||||
channel.sendControlMessage(new CompleteMessage()).syncUninterruptibly();
|
||||
sendControlMessage(new CompleteMessage()).syncUninterruptibly();
|
||||
closeSession(State.COMPLETE);
|
||||
}
|
||||
|
||||
|
|
@ -1061,7 +1097,7 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
// pass the session planId/index to the OFM (which is only set at init(), after the transfers have already been created)
|
||||
ofm.header.addSessionInfo(this);
|
||||
// do not sync here as this does disk access
|
||||
channel.sendControlMessage(ofm);
|
||||
sendControlMessage(ofm);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -1164,7 +1200,7 @@ public class StreamSession implements IEndpointStateChangeSubscriber
|
|||
logger.info("[Stream #{}] Aborting stream session with peer {}...", planId(), peer);
|
||||
|
||||
if (channel.connected())
|
||||
channel.sendControlMessage(new SessionFailedMessage());
|
||||
sendControlMessage(new SessionFailedMessage());
|
||||
|
||||
try
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1579,6 +1579,11 @@ public class NodeProbe implements AutoCloseable
|
|||
return spProxy.getReadRepairRepairedBackground();
|
||||
}
|
||||
|
||||
public Map<String,long[]> getOutOfRangeOpCounts()
|
||||
{
|
||||
return ssProxy.getOutOfRangeOperationCounts();
|
||||
}
|
||||
|
||||
// JMX getters for the o.a.c.metrics API below.
|
||||
/**
|
||||
* Retrieve cache metrics based on the cache type (KeyCache, RowCache, or CounterCache)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ public class Info extends NodeToolCmd
|
|||
@Option(name = {"-T", "--tokens"}, description = "Display all tokens")
|
||||
private boolean tokens = false;
|
||||
|
||||
@Option(name = {"-O", "--out-of-range-ops"}, description = "Display per-keyspace counts of operations for invalid tokens")
|
||||
private boolean outOfRangeOps = false;
|
||||
|
||||
@Override
|
||||
public void execute(NodeProbe probe)
|
||||
{
|
||||
|
|
@ -175,6 +178,20 @@ public class Info extends NodeToolCmd
|
|||
{
|
||||
out.printf("%-23s: (node is not joined to the cluster)%n", "Token");
|
||||
}
|
||||
|
||||
// Operations for out of range tokens
|
||||
if (this.outOfRangeOps)
|
||||
{
|
||||
System.out.printf("%-23s: %-48s %10s %10s %10s%n", "Invalid Token Ops", "Keyspace", "Read", "Write", "Paxos");
|
||||
|
||||
Map<String, long[]> outOfRangeOpCounts = probe.getOutOfRangeOpCounts();
|
||||
outOfRangeOpCounts.forEach((ks, counts) -> System.out.printf("%-24s %-48s %10s %10s %10s%n",
|
||||
"",
|
||||
ks,
|
||||
counts[0],
|
||||
counts[1],
|
||||
counts[2]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -874,6 +874,11 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance
|
|||
throw e;
|
||||
}
|
||||
};
|
||||
error = parallelRun(error, executor,
|
||||
// If an index build completes as shutting down, setIndexBuild may trigger
|
||||
// a CFS.forceBlockingFlush
|
||||
() -> SecondaryIndexManager.shutdownAndWait(1L, MINUTES)
|
||||
);
|
||||
error = parallelRun(error, executor,
|
||||
() -> Gossiper.instance.stopShutdownAndWait(1L, MINUTES),
|
||||
CompactionManager.instance::forceShutdown,
|
||||
|
|
@ -886,7 +891,6 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance
|
|||
() -> StreamReceiveTask.shutdownAndWait(1L, MINUTES),
|
||||
() -> StreamTransferTask.shutdownAndWait(1L, MINUTES),
|
||||
() -> StreamManager.instance.stop(),
|
||||
() -> SecondaryIndexManager.shutdownAndWait(1L, MINUTES),
|
||||
() -> IndexSummaryManager.instance.shutdownAndWait(1L, MINUTES),
|
||||
() -> ColumnFamilyStore.shutdownExecutorsAndWait(1L, MINUTES),
|
||||
() -> BufferPools.shutdownLocalCleaner(1L, MINUTES),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.distributed.test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.distributed.Cluster;
|
||||
import org.apache.cassandra.distributed.api.ConsistencyLevel;
|
||||
import org.apache.cassandra.distributed.api.ICluster;
|
||||
|
||||
import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
|
||||
import static org.apache.cassandra.distributed.shared.AssertUtils.row;
|
||||
|
||||
public class OutOfTokenRangeTest extends TestBaseImpl
|
||||
{
|
||||
private static ICluster<?> cluster = null;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupPartitions() throws IOException
|
||||
{
|
||||
cluster = init(Cluster.build().withNodes(2).start(), 2);
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck text, v text, PRIMARY KEY (pk, ck))");
|
||||
|
||||
cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk,ck,v) VALUES (1, 'a', '1a')", ConsistencyLevel.ALL);
|
||||
cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk,ck,v) VALUES (1, 'b', '1b')", ConsistencyLevel.ALL);
|
||||
cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk,ck,v) VALUES (1, 'c', '1c')", ConsistencyLevel.ALL);
|
||||
|
||||
// Prove the row is there before adding the index
|
||||
assertRows(cluster.coordinator(1).execute("SELECT pk, ck, v FROM " + KEYSPACE + ".tbl WHERE pk = 1 AND ck = 'a' ALLOW FILTERING", ConsistencyLevel.ALL), row(1, "a", "1a"));
|
||||
|
||||
cluster.schemaChange("CREATE INDEX ckindex ON " + KEYSPACE + ".tbl (ck)");
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void closeCluster() throws Exception
|
||||
{
|
||||
if (cluster != null)
|
||||
{
|
||||
cluster.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkSinglePartitionReadCommandTest()
|
||||
{
|
||||
Object[][] results = cluster.coordinator(1).execute("SELECT pk, ck, v FROM " + KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.ALL);
|
||||
assertRows(results, row(1, "a", "1a"), row(1, "b", "1b"), row(1, "c", "1c"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkPartitionRangeReadCommandTest()
|
||||
{
|
||||
Object[][] results = cluster.coordinator(1).execute("SELECT pk, ck, v FROM " + KEYSPACE + ".tbl WHERE pk = 1 AND ck = 'a'", ConsistencyLevel.ALL);
|
||||
assertRows(results, row(1, "a", "1a"));
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,6 @@ import java.util.concurrent.ExecutorService;
|
|||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.apache.cassandra.utils.concurrent.Condition;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
|
|
@ -56,6 +55,7 @@ import org.apache.cassandra.service.PendingRangeCalculatorService;
|
|||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.service.reads.repair.BlockingReadRepair;
|
||||
import org.apache.cassandra.service.reads.repair.ReadRepairStrategy;
|
||||
import org.apache.cassandra.utils.concurrent.Condition;
|
||||
|
||||
import static net.bytebuddy.matcher.ElementMatchers.named;
|
||||
|
||||
|
|
@ -206,6 +206,13 @@ public class ReadRepairTest extends TestBaseImpl
|
|||
PendingRangeCalculatorService.instance.blockUntilFinished();
|
||||
}).accept(cluster.get(2).broadcastAddress());
|
||||
|
||||
// mark #2 as leaving in #1
|
||||
cluster.get(1).acceptsOnInstance((InetSocketAddress endpoint) -> {
|
||||
StorageService.instance.getTokenMetadata().addLeavingEndpoint(InetAddressAndPort.getByAddressOverrideDefaults(endpoint.getAddress(), endpoint.getPort()));
|
||||
PendingRangeCalculatorService.instance.update();
|
||||
PendingRangeCalculatorService.instance.blockUntilFinished();
|
||||
}).accept(cluster.get(2).broadcastAddress());
|
||||
|
||||
// prevent #4 from reading or writing to #3, so our QUORUM must contain #2 and #4
|
||||
// since #1 is taking over the range, this means any read-repair must make it to #1 as well
|
||||
// (as a speculative repair in this case, as we prefer to send repair mutations to the initial
|
||||
|
|
@ -357,7 +364,8 @@ public class ReadRepairTest extends TestBaseImpl
|
|||
try (Cluster cluster = init(Cluster.build()
|
||||
.withConfig(config -> config.with(Feature.GOSSIP, Feature.NETWORK)
|
||||
.set("native_transport_timeout", String.format("%dms", Integer.MAX_VALUE))
|
||||
.set("read_request_timeout", String.format("%dms", Integer.MAX_VALUE)))
|
||||
.set("read_request_timeout", String.format("%dms", Integer.MAX_VALUE))
|
||||
.set("reject_out_of_token_range_requests", false))
|
||||
.withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4))
|
||||
.withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0"))
|
||||
.withNodes(3)
|
||||
|
|
@ -495,6 +503,7 @@ public class ReadRepairTest extends TestBaseImpl
|
|||
// on timestamp tie of RT and partition deletion: we should not generate RT bounds in such case,
|
||||
// since monotonicity is already ensured by the partition deletion, and RT is unnecessary there.
|
||||
// For details, see CASSANDRA-16453.
|
||||
@SuppressWarnings("unused")
|
||||
public static Object repairPartition(DecoratedKey partitionKey, Map<Replica, Mutation> mutations, ReplicaPlan.ForWrite writePlan, @SuperCall Callable<Void> r) throws Exception
|
||||
{
|
||||
Assert.assertEquals(2, mutations.size());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* 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.nio.ByteBuffer;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.SchemaLoader;
|
||||
import org.apache.cassandra.Util;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.context.CounterContext;
|
||||
import org.apache.cassandra.db.partitions.PartitionUpdate;
|
||||
import org.apache.cassandra.db.rows.BTreeRow;
|
||||
import org.apache.cassandra.db.rows.BufferCell;
|
||||
import org.apache.cassandra.db.rows.Cell;
|
||||
import org.apache.cassandra.db.rows.Row;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.exceptions.RequestFailureReason;
|
||||
import org.apache.cassandra.locator.Replica;
|
||||
import org.apache.cassandra.metrics.StorageMetrics;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.net.Verb;
|
||||
import org.apache.cassandra.schema.ColumnMetadata;
|
||||
import org.apache.cassandra.schema.KeyspaceParams;
|
||||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.MessageDelivery;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.broadcastAddress;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.bytesToken;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.node1;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.randomInt;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.registerOutgoingMessageSink;
|
||||
|
||||
public class CounterMutationVerbHandlerOutOfRangeTest
|
||||
{
|
||||
private static final String KEYSPACE = "CounterCacheTest";
|
||||
private static final String TABLE = "Counter1";
|
||||
|
||||
private CounterMutationVerbHandler handler;
|
||||
private ColumnFamilyStore cfs;
|
||||
private long startingTotalMetricCount;
|
||||
private long startingKeyspaceMetricCount;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() throws Exception
|
||||
{
|
||||
SchemaLoader.loadSchema();
|
||||
SchemaLoader.createKeyspace(KEYSPACE,
|
||||
KeyspaceParams.simple(1),
|
||||
SchemaLoader.counterCFMD(KEYSPACE, TABLE));
|
||||
StorageService.instance.initServer(0);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setLogOutOfTokenRangeRequests(true);
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(true);
|
||||
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(bytesToken(0), node1);
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(bytesToken(100), broadcastAddress);
|
||||
|
||||
MessagingService.instance().inboundSink.clear();
|
||||
MessagingService.instance().outboundSink.clear();
|
||||
|
||||
handler = new CounterMutationVerbHandler();
|
||||
cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE);
|
||||
cfs.truncateBlocking();
|
||||
startingKeyspaceMetricCount = keyspaceMetricValue();
|
||||
startingTotalMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptMutationForNaturalEndpoint() throws Exception
|
||||
{
|
||||
int messageId = randomInt();
|
||||
int value = randomInt();
|
||||
int key = 30;
|
||||
CounterMutation mutation = mutation(key, value);
|
||||
handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build());
|
||||
|
||||
// unlike non-counter mutations, we can't verify the response message for a successful write.
|
||||
// acting as the leader for the mutation, we'll try to forward the writes to the other replicas
|
||||
// but this will fail as the other node isn't really there. The response message is written
|
||||
// by the callback to these messages so it will never get sent. So the best we can do is to check
|
||||
// that the mutation was actually applied locally. When a counter mutation is rejected by the verb
|
||||
// handler we *can* verify the failure response message is sent.
|
||||
verifyWrite(key, value);
|
||||
assertEquals(startingTotalMetricCount, StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
assertEquals(startingKeyspaceMetricCount, keyspaceMetricValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptMutationForPendingEndpoint() throws Exception
|
||||
{
|
||||
// remove localhost from TM and add it back as pending
|
||||
StorageService.instance.getTokenMetadata().removeEndpoint(broadcastAddress);
|
||||
Multimap<Range<Token>, Replica> pending = HashMultimap.create();
|
||||
Range<Token> range = new Range<>(bytesToken(0), bytesToken(100));
|
||||
pending.put(range, new Replica(broadcastAddress, range, true));
|
||||
StorageService.instance.getTokenMetadata().setPendingRangesUnsafe(KEYSPACE, pending);
|
||||
|
||||
int messageId = randomInt();
|
||||
int value = randomInt();
|
||||
int key = 50;
|
||||
CounterMutation mutation = mutation(key, value);
|
||||
handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build());
|
||||
verifyWrite(key, value);
|
||||
assertEquals(startingTotalMetricCount, StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
assertEquals(startingKeyspaceMetricCount, keyspaceMetricValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectMutationForTokenOutOfRange() throws Exception
|
||||
{
|
||||
// reject a mutation for a token the node neither owns nor is pending
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int value = randomInt();
|
||||
int key = 200;
|
||||
CounterMutation mutation = mutation(key, value);
|
||||
handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build());
|
||||
verifyFailureResponse(messageSink, messageId);
|
||||
assertEquals(startingTotalMetricCount + 1, StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
assertEquals(startingKeyspaceMetricCount + 1, keyspaceMetricValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptMutationIfRejectionNotEnabled() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
int messageId = randomInt();
|
||||
int value = randomInt();
|
||||
int key = 500;
|
||||
CounterMutation mutation = mutation(key, value);
|
||||
// the node which is the actual natural endpoint for this mutation is not a real
|
||||
// node, but if we write at CL.ANY we'll generate a hint for it and StorageProxy's
|
||||
// counterWriterPerformer will blindly apply the mutation so we can verify it locally
|
||||
handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build());
|
||||
|
||||
verifyWrite(key, value);
|
||||
assertEquals(startingTotalMetricCount + 1, StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
assertEquals(startingKeyspaceMetricCount + 1, keyspaceMetricValue());
|
||||
}
|
||||
|
||||
private void verifyWrite(int key, int value)
|
||||
{
|
||||
ReadCommand read = Util.cmd(cfs, bytes(key)).build();
|
||||
ColumnMetadata col = cfs.metadata().getColumn(bytes("val"));
|
||||
assertEquals((long)value, CounterContext.instance().total(Util.getOnlyRow(read).getCell(col)));
|
||||
}
|
||||
|
||||
|
||||
private static void verifyFailureResponse(ListenableFuture<MessageDelivery> messageSink, int messageId ) throws Exception
|
||||
{
|
||||
MessageDelivery response = messageSink.get(100, TimeUnit.MILLISECONDS);
|
||||
assertEquals(Verb.FAILURE_RSP, response.message.verb());
|
||||
assertEquals(broadcastAddress, response.message.from());
|
||||
assertTrue(response.message.payload instanceof RequestFailureReason);
|
||||
assertEquals(messageId, response.message.id());
|
||||
assertEquals(node1, response.to);
|
||||
}
|
||||
|
||||
private CounterMutation mutation(int key, int columnValue)
|
||||
{
|
||||
TableMetadata cfm = Schema.instance.getTableMetadata(KEYSPACE, TABLE);
|
||||
DecoratedKey dk = cfs.decorateKey(bytes(key));
|
||||
ColumnMetadata col = cfs.metadata().getColumn(bytes("val"));
|
||||
ByteBuffer val = CounterContext.instance().createLocal(columnValue);
|
||||
Cell<?> counterCell = BufferCell.live(col, FBUtilities.timestampMicros(), val);
|
||||
Row row = BTreeRow.singleCellRow(cfs.metadata().comparator.make("clustering_1"), counterCell);
|
||||
PartitionUpdate update = PartitionUpdate.singleRowUpdate(cfm, dk, row);
|
||||
return new CounterMutation(new Mutation(update), ConsistencyLevel.ANY);
|
||||
}
|
||||
|
||||
private long keyspaceMetricValue()
|
||||
{
|
||||
return cfs.keyspace.metric.outOfRangeTokenWrites.getCount();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
* 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.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.SchemaLoader;
|
||||
import org.apache.cassandra.Util;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.partitions.PartitionUpdate;
|
||||
import org.apache.cassandra.db.rows.BTreeRow;
|
||||
import org.apache.cassandra.db.rows.BufferCell;
|
||||
import org.apache.cassandra.db.rows.Cell;
|
||||
import org.apache.cassandra.db.rows.Row;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.exceptions.RequestFailureReason;
|
||||
import org.apache.cassandra.locator.Replica;
|
||||
import org.apache.cassandra.metrics.StorageMetrics;
|
||||
import org.apache.cassandra.net.IVerbHandler;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.net.Verb;
|
||||
import org.apache.cassandra.schema.ColumnMetadata;
|
||||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.MessageDelivery;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.broadcastAddress;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.bytesToken;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.node1;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.randomInt;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.registerOutgoingMessageSink;
|
||||
|
||||
public class MutationVerbHandlerOutOfRangeTest
|
||||
{
|
||||
private static final String TEST_NAME = "mutation_vh_test_";
|
||||
private static final String KEYSPACE = TEST_NAME + "cql_keyspace";
|
||||
private static final String TABLE = "table1";
|
||||
|
||||
private ColumnFamilyStore cfs;
|
||||
private long startingTotalMetricCount;
|
||||
private long startingKeyspaceMetricCount;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() throws Exception
|
||||
{
|
||||
SchemaLoader.loadSchema();
|
||||
SchemaLoader.schemaDefinition(TEST_NAME);
|
||||
StorageService.instance.initServer(0);
|
||||
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setLogOutOfTokenRangeRequests(true);
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(true);
|
||||
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(bytesToken(0), node1);
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(bytesToken(100), broadcastAddress);
|
||||
|
||||
MessagingService.instance().inboundSink.clear();
|
||||
MessagingService.instance().outboundSink.clear();
|
||||
|
||||
cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE);
|
||||
startingKeyspaceMetricCount = keyspaceMetricValue(cfs);
|
||||
startingTotalMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptMutationForNaturalEndpoint() throws Exception
|
||||
{
|
||||
acceptMutationForNaturalEndpoint(new MutationVerbHandler());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptReadRepairForNaturalEndpoint() throws Exception
|
||||
{
|
||||
acceptMutationForNaturalEndpoint(new ReadRepairVerbHandler());
|
||||
}
|
||||
|
||||
private void acceptMutationForNaturalEndpoint(IVerbHandler<Mutation> handler) throws Exception
|
||||
{
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int value = randomInt();
|
||||
int key = 50;
|
||||
Mutation mutation = mutation(key, value);
|
||||
handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, messageId, key, value, false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptMutationForPendingEndpoint() throws Exception
|
||||
{
|
||||
acceptMutationForPendingEndpoint(new MutationVerbHandler());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptReadRepairForPendingEndpoint() throws Exception
|
||||
{
|
||||
acceptMutationForPendingEndpoint(new ReadRepairVerbHandler());
|
||||
}
|
||||
|
||||
private void acceptMutationForPendingEndpoint(IVerbHandler<Mutation> handler ) throws Exception
|
||||
{
|
||||
// remove localhost from TM and add it back as pending
|
||||
StorageService.instance.getTokenMetadata().removeEndpoint(broadcastAddress);
|
||||
Multimap<Range<Token>, Replica> pending = HashMultimap.create();
|
||||
Range<Token> range = new Range<>(bytesToken(0), bytesToken(100));
|
||||
pending.put(range, new Replica(broadcastAddress, range, true));
|
||||
StorageService.instance.getTokenMetadata().setPendingRangesUnsafe(KEYSPACE, pending);
|
||||
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int value = randomInt();
|
||||
int key = 50;
|
||||
Mutation mutation = mutation(key, value);
|
||||
handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, messageId, key, value, false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectMutationForTokenOutOfRange() throws Exception
|
||||
{
|
||||
rejectMutationForTokenOutOfRange(new MutationVerbHandler());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectReadRepairForTokenOutOfRange() throws Exception
|
||||
{
|
||||
rejectMutationForTokenOutOfRange(new ReadRepairVerbHandler());
|
||||
}
|
||||
|
||||
private void rejectMutationForTokenOutOfRange(IVerbHandler<Mutation> handler) throws Exception
|
||||
{
|
||||
// reject a mutation for a token the node neither owns nor is pending
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int value = randomInt();
|
||||
int key = 200;
|
||||
Mutation mutation = mutation(key, value);
|
||||
handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, messageId, key, value, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptMutationIfRejectionNotEnabled() throws Exception
|
||||
{
|
||||
acceptMutationIfRejectionNotEnabled(new MutationVerbHandler());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptReadRepairIfRejectionNotEnabled() throws Exception
|
||||
{
|
||||
acceptMutationIfRejectionNotEnabled(new ReadRepairVerbHandler());
|
||||
}
|
||||
|
||||
private void acceptMutationIfRejectionNotEnabled(IVerbHandler<Mutation> handler) throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int value = randomInt();
|
||||
int key = 200;
|
||||
Mutation mutation = mutation(key, value);
|
||||
handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, messageId, key, value, true, false);
|
||||
}
|
||||
|
||||
static <V> int toInt(Cell<V> cell)
|
||||
{
|
||||
return cell.accessor().toInt(cell.value());
|
||||
}
|
||||
|
||||
private void getAndVerifyResponse(ListenableFuture<MessageDelivery> messageSink,
|
||||
int messageId,
|
||||
int key,
|
||||
int value,
|
||||
boolean isOutOfRange,
|
||||
boolean expectFailure) throws InterruptedException, ExecutionException, TimeoutException
|
||||
{
|
||||
MessageDelivery response = messageSink.get(100, TimeUnit.MILLISECONDS);
|
||||
assertEquals(expectFailure ? Verb.FAILURE_RSP : Verb.MUTATION_RSP, response.message.verb());
|
||||
assertEquals(broadcastAddress, response.message.from());
|
||||
assertEquals(expectFailure, response.message.payload instanceof RequestFailureReason);
|
||||
assertEquals(messageId, response.message.id());
|
||||
assertEquals(node1, response.to);
|
||||
assertEquals(startingTotalMetricCount + (isOutOfRange ? 1 : 0), StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
assertEquals(startingKeyspaceMetricCount + (isOutOfRange ? 1 : 0), keyspaceMetricValue(cfs));
|
||||
if (!expectFailure)
|
||||
{
|
||||
ReadCommand read = Util.cmd(cfs, bytes(key)).build();
|
||||
ColumnMetadata col = cfs.metadata().getColumn(bytes("v1"));
|
||||
assertEquals(value, toInt(Util.getOnlyRow(read).getCell(col)));
|
||||
}
|
||||
}
|
||||
|
||||
private static long keyspaceMetricValue(ColumnFamilyStore cfs)
|
||||
{
|
||||
return cfs.keyspace.metric.outOfRangeTokenWrites.getCount();
|
||||
}
|
||||
|
||||
private Mutation mutation(int key, int columnValue)
|
||||
{
|
||||
TableMetadata cfm = Schema.instance.getTableMetadata(KEYSPACE, TABLE);
|
||||
DecoratedKey dk = cfs.decorateKey(bytes(key));
|
||||
ColumnMetadata col = cfs.metadata().getColumn(bytes("v1"));
|
||||
Cell<?> cell = BufferCell.live(col, FBUtilities.timestampMicros(), bytes(columnValue));
|
||||
Row row = BTreeRow.singleCellRow(Clustering.EMPTY, cell);
|
||||
PartitionUpdate update = PartitionUpdate.singleRowUpdate(cfm, dk, row);
|
||||
return new Mutation(update);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
/*
|
||||
* 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.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.SchemaLoader;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
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.partitions.UnfilteredPartitionIterator;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.metrics.StorageMetrics;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.net.Verb;
|
||||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
import org.apache.cassandra.utils.TokenRangeTestUtil;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import static org.apache.cassandra.net.Verb.READ_REQ;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.broadcastAddress;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.bytesToken;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.node1;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.randomInt;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.registerOutgoingMessageSink;
|
||||
|
||||
public class ReadCommandVerbHandlerOutOfRangeTest
|
||||
{
|
||||
private static ReadCommandVerbHandler handler;
|
||||
private static TableMetadata metadata_nonreplicated;
|
||||
private ColumnFamilyStore cfs;
|
||||
private long startingTotalMetricCount;
|
||||
private long startingKeyspaceMetricCount;
|
||||
|
||||
private static final String TEST_NAME = "read_command_vh_test_";
|
||||
private static final String KEYSPACE_NONREPLICATED = TEST_NAME + "cql_keyspace";
|
||||
private static final String TABLE = "table1";
|
||||
|
||||
@BeforeClass
|
||||
public static void init() throws Throwable
|
||||
{
|
||||
SchemaLoader.loadSchema();
|
||||
SchemaLoader.schemaDefinition(TEST_NAME);
|
||||
StorageService.instance.initServer(0);
|
||||
metadata_nonreplicated = Schema.instance.getTableMetadata(KEYSPACE_NONREPLICATED, TABLE);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup()
|
||||
{
|
||||
DatabaseDescriptor.setLogOutOfTokenRangeRequests(true);
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(true);
|
||||
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(bytesToken(0), node1);
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(bytesToken(100), broadcastAddress);
|
||||
|
||||
MessagingService.instance().inboundSink.clear();
|
||||
MessagingService.instance().outboundSink.clear();
|
||||
MessagingService.instance().outboundSink.add((message, to) -> false);
|
||||
MessagingService.instance().inboundSink.add((message) -> false);
|
||||
|
||||
cfs = Keyspace.open(KEYSPACE_NONREPLICATED).getColumnFamilyStore(TABLE);
|
||||
startingKeyspaceMetricCount = keyspaceMetricValue(cfs);
|
||||
startingTotalMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount();
|
||||
handler = new ReadCommandVerbHandler();
|
||||
}
|
||||
|
||||
private static DecoratedKey key(TableMetadata metadata, int key)
|
||||
{
|
||||
return metadata.partitioner.decorateKey(ByteBufferUtil.bytes(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptReadForNaturalEndpoint() throws Exception
|
||||
{
|
||||
ListenableFuture<TokenRangeTestUtil.MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int key = 50;
|
||||
ReadCommand command = command(key);
|
||||
handler.doVerb(Message.builder(READ_REQ, command).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, messageId, false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectReadForTokenOutOfRange() throws Exception
|
||||
{
|
||||
// reject a read for a key who's token the node doesn't own the range for
|
||||
ListenableFuture<TokenRangeTestUtil.MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int key = 200;
|
||||
ReadCommand command = command(key);
|
||||
handler.doVerb(Message.builder(READ_REQ, command).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, messageId, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptReadIfRejectionNotEnabled() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
ListenableFuture<TokenRangeTestUtil.MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int key = 200;
|
||||
ReadCommand command = command(key);
|
||||
handler.doVerb(Message.builder(READ_REQ, command).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, messageId, true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rangeReadCommandBoundsAreNotChecked() throws Exception
|
||||
{
|
||||
// checking is only currently done for single partition reads, range reads will continue to
|
||||
// accept any range they are given. So for a range wholly outside the node's ownership we
|
||||
// expect the metric to remain unchanged and read command to be executed.
|
||||
// This test is added for 3.0 because the single partition & range commands are now processed
|
||||
// by the same verb handler.
|
||||
// rdar://problem/33535104 is to extend checking to range reads
|
||||
ListenableFuture<TokenRangeTestUtil.MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
Range<Token> range = new Range<>(key(metadata_nonreplicated, 150).getToken(),
|
||||
key(metadata_nonreplicated, 160).getToken());
|
||||
ReadCommand command = new StubRangeReadCommand(range, metadata_nonreplicated);
|
||||
handler.doVerb(Message.builder(READ_REQ, command).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, messageId, false, false);
|
||||
}
|
||||
|
||||
private void getAndVerifyResponse(ListenableFuture<TokenRangeTestUtil.MessageDelivery> messageSink,
|
||||
int messageId,
|
||||
boolean isOutOfRange,
|
||||
boolean expectFailure) throws InterruptedException, ExecutionException, TimeoutException
|
||||
{
|
||||
assertEquals(startingTotalMetricCount + (isOutOfRange ? 1 : 0), StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
assertEquals(startingKeyspaceMetricCount + (isOutOfRange ? 1 : 0), keyspaceMetricValue(cfs));
|
||||
if (expectFailure)
|
||||
{
|
||||
try
|
||||
{
|
||||
TokenRangeTestUtil.MessageDelivery response = messageSink.get(10, TimeUnit.MILLISECONDS);
|
||||
fail(String.format("Didn't expect any message to be sent, but sent %s to %s in response to %s",
|
||||
response.message.toString(),
|
||||
response.to,
|
||||
response.message.id()));
|
||||
}
|
||||
catch (TimeoutException e)
|
||||
{
|
||||
// expected
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TokenRangeTestUtil.MessageDelivery response = messageSink.get(10, TimeUnit.MILLISECONDS);
|
||||
assertEquals(Verb.READ_RSP, response.message.verb());
|
||||
assertEquals(broadcastAddress, response.message.from());
|
||||
assertEquals(messageId, response.message.id());
|
||||
assertEquals(node1, response.to);
|
||||
}
|
||||
}
|
||||
|
||||
private ReadCommand command(int key)
|
||||
{
|
||||
return new StubReadCommand(key, metadata_nonreplicated);
|
||||
}
|
||||
|
||||
private static class StubReadCommand extends SinglePartitionReadCommand
|
||||
{
|
||||
private final TableMetadata tmd;
|
||||
|
||||
StubReadCommand(int key, TableMetadata tmd)
|
||||
{
|
||||
super(false,
|
||||
0,
|
||||
false,
|
||||
tmd,
|
||||
FBUtilities.nowInSeconds(),
|
||||
ColumnFilter.all(tmd),
|
||||
RowFilter.NONE,
|
||||
DataLimits.NONE,
|
||||
key(tmd, key),
|
||||
null,
|
||||
null,
|
||||
false);
|
||||
|
||||
this.tmd = tmd;
|
||||
}
|
||||
|
||||
public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController)
|
||||
{
|
||||
return EmptyIterators.unfilteredPartition(tmd);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return "<<StubReadCommand>>";
|
||||
}
|
||||
}
|
||||
|
||||
private static class StubRangeReadCommand extends PartitionRangeReadCommand
|
||||
{
|
||||
private final TableMetadata cfm;
|
||||
|
||||
StubRangeReadCommand(Range<Token> range, TableMetadata tmd)
|
||||
{
|
||||
super(false,
|
||||
0,
|
||||
false,
|
||||
tmd,
|
||||
FBUtilities.nowInSeconds(),
|
||||
ColumnFilter.all(tmd),
|
||||
RowFilter.NONE,
|
||||
DataLimits.NONE,
|
||||
DataRange.forTokenRange(range),
|
||||
null,
|
||||
false);
|
||||
|
||||
this.cfm = tmd;
|
||||
}
|
||||
|
||||
public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController)
|
||||
{
|
||||
return EmptyIterators.unfilteredPartition(cfm);
|
||||
}
|
||||
}
|
||||
|
||||
private static long keyspaceMetricValue(ColumnFamilyStore cfs)
|
||||
{
|
||||
return cfs.keyspace.metric.outOfRangeTokenReads.getCount();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* 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.dht;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.generateRanges;
|
||||
|
||||
public class OwnedRangesTest
|
||||
{
|
||||
@Test
|
||||
public void testFilterRangesWithEmptySuperset()
|
||||
{
|
||||
List<Range<Token>> toFilter = generateRanges(0, 50);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(Collections.emptyList()).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithEmptySupersetEmptySubset()
|
||||
{
|
||||
assertTrue(new OwnedRanges(Collections.emptyList()).testRanges(Collections.emptyList()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetEmptySubset()
|
||||
{
|
||||
assertTrue(new OwnedRanges(generateRanges(0, 100)).testRanges(Collections.emptyList()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetSingleSubsetContained()
|
||||
{
|
||||
assertTrue(new OwnedRanges(generateRanges(0, 100)).testRanges(generateRanges(10 , 20)).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetSingleSubsetExactMatch()
|
||||
{
|
||||
assertTrue(new OwnedRanges(generateRanges(0, 100)).testRanges(generateRanges(0, 100)).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetSingleSubsetStrictlyLessThan()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(-100, 0);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetSingleSubsetStrictlyGreaterThan()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(101, 200);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetSingleSubsetOverlapping()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(80, 120);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetSingleSubsetOverlappingOnRight()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(0, 120);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetSingleSubsetOverlappingOnLeft()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(-10, 100);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetMultipleSubsetAllContained()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(10, 20, 30, 40, 50, 60);
|
||||
assertTrue(new OwnedRanges(generateRanges(0, 100)).testRanges(toFilter).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetMultipleSubsetSomeContained()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(-20, -10, 0, 10, 20, 30, 110, 120);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100)).testRanges(toFilter);
|
||||
assertRangesEqual(generateRanges(-20, -10, 110, 120), rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithSingleSupersetMultipleSubsetNoneContained()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(-40, -20, -20, -10, 110, 120, 230, 140);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterWithMultipleSupersetSingleSubsetContainedInFirst()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(10, 20);
|
||||
assertTrue(new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterWithMultipleSupersetSingleSubsetContainedInLast()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(450, 460);
|
||||
assertTrue(new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithMultipleSupersetSingleSubsetStrictlyGreaterThan()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(510, 520);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithMultipleSupersetSingleSubsetStrictlyLessThan()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(-20, -10);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithMultipleSupersetSingleSubsetOverlapping()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(80, 120);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithMultipleSupersetSingleSubsetOverlappingOnRight()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(0, 120);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithMultipleSupersetSingleSubsetOverlappingOnLeft()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(-10, 100);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithMultipleSupersetMultipleSubsetAllDisjoint()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(-20, -10, 110, 120, 310, 320);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter);
|
||||
assertRangesEqual(toFilter, rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithMultipleSupersetMultipleSubsetAllContained()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(10, 20, 210, 220, 410, 420);
|
||||
assertTrue(new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithMultipleSupersetMultipleSubsetSomeContained()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(10, 20, 210, 220, 310, 320, 410, 420);
|
||||
Collection<Range<Token>> rejected = new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter);
|
||||
assertRangesEqual(rejected, generateRanges(310, 320));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterRangesWithMultipleSupersetMultipleSubsetSomeExactMatchSomeContained()
|
||||
{
|
||||
Collection<Range<Token>> toFilter = generateRanges(10, 20, 200, 300, 410, 420);
|
||||
assertTrue(new OwnedRanges(generateRanges(0, 100, 200, 300, 400, 500)).testRanges(toFilter).isEmpty());
|
||||
}
|
||||
|
||||
private static void assertRangesEqual(Collection<Range<Token>> first, Collection<Range<Token>> second)
|
||||
{
|
||||
assertTrue(CollectionUtils.isEqualCollection(Range.normalize(first), Range.normalize(second)));
|
||||
}
|
||||
}
|
||||
|
|
@ -20,21 +20,24 @@
|
|||
*/
|
||||
package org.apache.cassandra.locator;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PendingRangeMapsTest {
|
||||
|
||||
public class PendingRangeMapsTest
|
||||
{
|
||||
private Range<Token> genRange(String left, String right)
|
||||
{
|
||||
return new Range<Token>(new BigIntegerToken(left), new BigIntegerToken(right));
|
||||
return new Range<>(new BigIntegerToken(left), new BigIntegerToken(right));
|
||||
}
|
||||
|
||||
private static void addPendingRange(PendingRangeMaps pendingRangeMaps, Range<Token> range, String endpoint)
|
||||
|
|
@ -105,4 +108,60 @@ public class PendingRangeMapsTest {
|
|||
assertTrue(replicas.endpoints().contains(InetAddressAndPort.getByName("127.0.0.1")));
|
||||
assertTrue(replicas.endpoints().contains(InetAddressAndPort.getByName("127.0.0.7")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsTokenInLocalPendingRanges()
|
||||
{
|
||||
PendingRangeMaps pendingRangeMaps = new PendingRangeMaps();
|
||||
InetAddressAndPort self = FBUtilities.getBroadcastAddressAndPort();
|
||||
String selfHostAddress = self.getHostAddress(true);
|
||||
|
||||
addPendingRange(pendingRangeMaps, genRange("5", "15"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange("15", "25"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange("25", "35"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange("35", "45"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange("45", "55"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange("45", "65"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange( "66", "67"), "127.0.0.7");
|
||||
|
||||
assertFalse(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("0")));
|
||||
assertFalse(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("5")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("10")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("15")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("20")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("25")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("35")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("45")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("55")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("65")));
|
||||
assertFalse(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("66")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsTokenInLocalPendingRangesWrapAroundRanges()
|
||||
{
|
||||
PendingRangeMaps pendingRangeMaps = new PendingRangeMaps();
|
||||
InetAddressAndPort self = FBUtilities.getBroadcastAddressAndPort();
|
||||
String selfHostAddress = self.getHostAddress(true);
|
||||
|
||||
addPendingRange(pendingRangeMaps, genRange("5", "15"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange("15", "25"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange("25", "35"), "127.0.0.1");
|
||||
addPendingRange(pendingRangeMaps, genRange("35", "45"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange("45", "55"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange("45", "65"), selfHostAddress);
|
||||
addPendingRange(pendingRangeMaps, genRange("65", "7"), selfHostAddress);
|
||||
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("0")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("5")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("7")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("10")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("15")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("20")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("25")));
|
||||
assertFalse(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("35")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("45")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("55")));
|
||||
assertTrue(pendingRangeMaps.isTokenInLocalPendingRange(new BigIntegerToken("65")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,7 +358,6 @@ public class TokenMetadataTest
|
|||
assertEquals(0, tokenMetadata.getSizeOfMovingEndpoints());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testRemoveEndpointTokenChange() throws Exception
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,270 @@
|
|||
/*
|
||||
* 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.repair;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.SchemaLoader;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.exceptions.RequestFailureReason;
|
||||
import org.apache.cassandra.metrics.StorageMetrics;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.net.Verb;
|
||||
import org.apache.cassandra.repair.messages.PrepareMessage;
|
||||
import org.apache.cassandra.repair.messages.RepairMessage;
|
||||
import org.apache.cassandra.repair.messages.ValidationResponse;
|
||||
import org.apache.cassandra.repair.messages.ValidationRequest;
|
||||
import org.apache.cassandra.repair.state.ParticipateState;
|
||||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.schema.TableId;
|
||||
import org.apache.cassandra.service.ActiveRepairService;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.streaming.PreviewKind;
|
||||
import org.apache.cassandra.utils.TimeUUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.MessageDelivery;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.broadcastAddress;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.generateRange;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.generateRanges;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.node1;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.randomInt;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.registerOutgoingMessageSink;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.setLocalTokens;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.token;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.uuid;
|
||||
|
||||
public class RepairMessageVerbHandlerOutOfRangeTest
|
||||
{
|
||||
|
||||
private static final String TEST_NAME = "repair_message_vh_test_";
|
||||
private static final String KEYSPACE = TEST_NAME + "cql_keyspace";
|
||||
private static final String TABLE = "table1";
|
||||
private static List<TableId> tableIds;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
|
||||
SchemaLoader.loadSchema();
|
||||
SchemaLoader.schemaDefinition(TEST_NAME);
|
||||
StorageService.instance.initServer(0);
|
||||
tableIds = Collections.singletonList(Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE).metadata().id);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setLogOutOfTokenRangeRequests(true);
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(true);
|
||||
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
// All tests suppose a 2 node ring, with the other peer having the tokens 0, 200, 300
|
||||
// Initially, the local node has no tokens so when indivividual test set owned tokens or
|
||||
// pending ranges for the local node, they're always in relation to this.
|
||||
// e.g. test calls setLocalTokens(100, 300) the ring now looks like
|
||||
// peer -> (min, 0], (100, 200], (300, 400]
|
||||
// local -> (0, 100], (200, 300], (400, max]
|
||||
//
|
||||
// Pending ranges are set in test using start/end pairs.
|
||||
// Ring is initialised:
|
||||
// peer -> (min, max]
|
||||
// local -> (,]
|
||||
// e.g. test calls setPendingRanges(0, 100, 200, 300)
|
||||
// the pending ranges for local would be calculated as:
|
||||
// local -> (0, 100], (200, 300]
|
||||
StorageService.instance.getTokenMetadata().updateNormalTokens(Lists.newArrayList(token(0),
|
||||
token(200),
|
||||
token(400)),
|
||||
node1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrepareWithAllRequestedRangesWithinOwned() throws Exception
|
||||
{
|
||||
setLocalTokens(100);
|
||||
PrepareMessage prepare = prepareMsg(generateRanges(10, 20));
|
||||
tryPrepareExpectingSuccess(prepare);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
prepare = prepareMsg(generateRanges(10, 20)); // recreate same request with unused parent id
|
||||
tryPrepareExpectingSuccess(prepare);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrepareWithAllRequestedRangesOutsideOwned() throws Exception
|
||||
{
|
||||
setLocalTokens(100);
|
||||
PrepareMessage prepare = prepareMsg(generateRanges(110, 120));
|
||||
tryPrepareExpectingSuccess(prepare);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
prepare = prepareMsg(generateRanges(110, 120)); // recreate same request with unused parent id
|
||||
tryPrepareExpectingSuccess(prepare);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrepareWithSomeRequestedRangesOutsideOwned() throws Exception
|
||||
{
|
||||
setLocalTokens(100);
|
||||
PrepareMessage prepare = prepareMsg(generateRanges(10, 20, 110, 120));
|
||||
tryPrepareExpectingSuccess(prepare);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
prepare = prepareMsg(generateRanges(10, 20, 110, 120)); // recreate same request with unused parent id
|
||||
tryPrepareExpectingSuccess(prepare);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidationRequestWithRequestedRangeWithinOwned() throws Exception
|
||||
{
|
||||
setLocalTokens(100);
|
||||
ValidationRequest request = validationMsg(generateRange(10, 20));
|
||||
tryValidationExpectingSuccess(request, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
request = validationMsg(generateRange(10, 20)); // recreate same request with unused parent id
|
||||
tryValidationExpectingSuccess(request, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidationRequestWithRequestedRangeOutsideOwned() throws Exception
|
||||
{
|
||||
setLocalTokens(100);
|
||||
ValidationRequest request = validationMsg(generateRange(110, 120));
|
||||
tryValidationExpectingFailure(request);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
request = validationMsg(generateRange(110, 120)); // recreate same request with unused parent id
|
||||
tryValidationExpectingSuccess(request, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidationRequestWithRequestedRangeOverlappingOwned() throws Exception
|
||||
{
|
||||
setLocalTokens(100);
|
||||
ValidationRequest request = validationMsg(generateRange(10, 120));
|
||||
tryValidationExpectingFailure(request);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
request = validationMsg(generateRange(10, 120)); // recreate same request with unused parent id
|
||||
tryValidationExpectingSuccess(request, true);
|
||||
}
|
||||
|
||||
private static void tryValidationExpectingFailure(ValidationRequest request) throws Exception
|
||||
{
|
||||
tryValidation(request, true, false);
|
||||
}
|
||||
|
||||
private static void tryValidationExpectingSuccess(ValidationRequest request, boolean isOutOfRange) throws Exception
|
||||
{
|
||||
tryValidation(request, isOutOfRange, true);
|
||||
}
|
||||
|
||||
private static void tryValidation(ValidationRequest request, boolean isOutOfRange, boolean expectSuccess) throws Exception
|
||||
{
|
||||
long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount();
|
||||
MessagingService.instance().outboundSink.clear();
|
||||
MessagingService.instance().inboundSink.clear();
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink(true);
|
||||
RepairMessageVerbHandler handler = new RepairMessageVerbHandler();
|
||||
int messageId = randomInt();
|
||||
// message must be prepared first as validate checks it is registered.
|
||||
PrepareMessage prepare = prepareMsg(request.desc.parentSessionId, request.desc.ranges);
|
||||
ActiveRepairService.instance.register(new ParticipateState(node1, prepare));
|
||||
Message<RepairMessage> message = Message.builder(Verb.VALIDATION_REQ, (RepairMessage)request).from(node1).withId(messageId).build();
|
||||
handler.doVerb(message);
|
||||
// Then sends the real response
|
||||
MessageDelivery response = messageSink.get(500, TimeUnit.MILLISECONDS);
|
||||
assertEquals(Verb.VALIDATION_RSP, response.message.verb());
|
||||
assertEquals(broadcastAddress, response.message.from());
|
||||
assertEquals(node1, response.to);
|
||||
assertTrue(response.message.payload instanceof ValidationResponse);
|
||||
ValidationResponse completion = (ValidationResponse) response.message.payload;
|
||||
assertEquals(expectSuccess, completion.success());
|
||||
assertEquals(startMetricCount + (isOutOfRange ? 1 : 0), StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
}
|
||||
|
||||
private static void tryPrepareExpectingSuccess(PrepareMessage prepare) throws Exception
|
||||
{
|
||||
long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount();
|
||||
MessagingService.instance().outboundSink.clear();
|
||||
MessagingService.instance().inboundSink.clear();
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink(false);
|
||||
RepairMessageVerbHandler handler = new RepairMessageVerbHandler();
|
||||
int messageId = randomInt();
|
||||
Message<RepairMessage> message = Message.builder(Verb.PREPARE_MSG, (RepairMessage)prepare).from(node1).withId(messageId).build();
|
||||
handler.doVerb(message);
|
||||
|
||||
MessageDelivery response = messageSink.get(100, TimeUnit.MILLISECONDS);
|
||||
assertEquals(Verb.REPAIR_RSP, response.message.verb());
|
||||
assertEquals(broadcastAddress, response.message.from());
|
||||
assertEquals(messageId, response.message.id());
|
||||
assertEquals(node1, response.to);
|
||||
assertFalse(response.message.payload instanceof RequestFailureReason);
|
||||
assertEquals(startMetricCount, StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
}
|
||||
|
||||
private static PrepareMessage prepareMsg(Collection<Range<Token>> ranges)
|
||||
{
|
||||
return prepareMsg(uuid(), ranges);
|
||||
}
|
||||
private static PrepareMessage prepareMsg(TimeUUID parentRepairSession, Collection<Range<Token>> ranges)
|
||||
{
|
||||
return new PrepareMessage(parentRepairSession, tableIds, ranges, false, ActiveRepairService.UNREPAIRED_SSTABLE, true, PreviewKind.NONE);
|
||||
}
|
||||
|
||||
private static ValidationRequest validationMsg(Range<Token> range)
|
||||
{
|
||||
TimeUUID parentId = uuid();
|
||||
List<ColumnFamilyStore> stores = tableIds.stream()
|
||||
.map(Schema.instance::getColumnFamilyStoreInstance)
|
||||
.collect(Collectors.toList());
|
||||
ActiveRepairService.instance.registerParentRepairSession(parentId,
|
||||
node1,
|
||||
stores,
|
||||
Collections.singleton(range),
|
||||
false,
|
||||
ActiveRepairService.UNREPAIRED_SSTABLE,
|
||||
true,
|
||||
PreviewKind.NONE);
|
||||
return new ValidationRequest(new RepairJobDesc(parentId, uuid(), KEYSPACE, TABLE, Collections.singleton(range)),
|
||||
randomInt());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* 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.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.SchemaLoader;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.DecoratedKey;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.exceptions.RequestFailureReason;
|
||||
import org.apache.cassandra.locator.Replica;
|
||||
import org.apache.cassandra.metrics.StorageMetrics;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.net.Verb;
|
||||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.service.paxos.v1.AbstractPaxosVerbHandler;
|
||||
import org.apache.cassandra.service.paxos.v1.PrepareVerbHandler;
|
||||
import org.apache.cassandra.service.paxos.v1.ProposeVerbHandler;
|
||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.MessageDelivery;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.broadcastAddress;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.bytesToken;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.node1;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.randomInt;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.registerOutgoingMessageSink;
|
||||
|
||||
// PaxosV1 out of range tests
|
||||
public class PaxosVerbHandlerOutOfRangeTest
|
||||
{
|
||||
// For the purposes of this testing, the details of the Commit don't really matter
|
||||
// as we're just testing the rejection (or lack of) and not the result of doing
|
||||
// whatever the specific verb handlers are supposed to do when they don't reject
|
||||
// a given Commit
|
||||
|
||||
private static final String TEST_NAME = "paxos_vh_test_";
|
||||
private static final String KEYSPACE = TEST_NAME + "cql_keyspace";
|
||||
private static final String TABLE = "table1";
|
||||
|
||||
private long startingTotalMetricCount;
|
||||
private long startingKeyspaceMetricCount;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() throws Exception
|
||||
{
|
||||
SchemaLoader.loadSchema();
|
||||
SchemaLoader.schemaDefinition(TEST_NAME);
|
||||
StorageService.instance.initServer(0);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setLogOutOfTokenRangeRequests(true);
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(true);
|
||||
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(bytesToken(0), node1);
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(bytesToken(100), broadcastAddress);
|
||||
|
||||
MessagingService.instance().inboundSink.clear();
|
||||
MessagingService.instance().outboundSink.clear();
|
||||
startingTotalMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount();
|
||||
startingKeyspaceMetricCount = keyspaceMetricValue();
|
||||
}
|
||||
private static DecoratedKey key(TableMetadata metadata, int key)
|
||||
{
|
||||
return metadata.partitioner.decorateKey(ByteBufferUtil.bytes(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptPrepareForNaturalEndpoint() throws Exception
|
||||
{
|
||||
acceptRequestForNaturalEndpoint(Verb.PAXOS_PREPARE_REQ, Verb.PAXOS_PREPARE_RSP, new PrepareVerbHandler());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptProposeForNaturalEndpoint() throws Exception
|
||||
{
|
||||
acceptRequestForNaturalEndpoint(Verb.PAXOS_PROPOSE_REQ, Verb.PAXOS_PROPOSE_RSP, new ProposeVerbHandler());
|
||||
}
|
||||
|
||||
private void acceptRequestForNaturalEndpoint(Verb requestVerb, Verb responseVerb, AbstractPaxosVerbHandler handler) throws Exception
|
||||
{
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int key = 50;
|
||||
Commit commit = commit(key);
|
||||
handler.doVerb(Message.builder(requestVerb, commit).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, responseVerb, messageId, false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptPrepareForPendingEndpoint() throws Exception
|
||||
{
|
||||
acceptRequestForPendingEndpoint(Verb.PAXOS_PREPARE_REQ, Verb.PAXOS_PREPARE_RSP, new PrepareVerbHandler());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptProposeForPendingEndpoint() throws Exception
|
||||
{
|
||||
acceptRequestForPendingEndpoint(Verb.PAXOS_PROPOSE_REQ, Verb.PAXOS_PROPOSE_RSP, new ProposeVerbHandler());
|
||||
}
|
||||
|
||||
private void acceptRequestForPendingEndpoint(Verb requestVerb, Verb responseVerb, AbstractPaxosVerbHandler handler) throws Exception
|
||||
{
|
||||
// remove localhost from TM and add it back as pending
|
||||
StorageService.instance.getTokenMetadata().removeEndpoint(broadcastAddress);
|
||||
Multimap<Range<Token>, Replica> pending = HashMultimap.create();
|
||||
Range<Token> range = new Range<>(bytesToken(0), bytesToken(100));
|
||||
pending.put(range, new Replica(broadcastAddress, range, true));
|
||||
StorageService.instance.getTokenMetadata().setPendingRangesUnsafe(KEYSPACE, pending);
|
||||
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int key = 50;
|
||||
Commit commit = commit(key);
|
||||
handler.doVerb(Message.builder(requestVerb, commit).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, responseVerb, messageId, false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectPrepareForTokenOutOfRange() throws Exception
|
||||
{
|
||||
rejectRequestForTokenOutOfRange(Verb.PAXOS_PREPARE_REQ, Verb.FAILURE_RSP, new PrepareVerbHandler());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectProposeForTokenOutOfRange() throws Exception
|
||||
{
|
||||
rejectRequestForTokenOutOfRange(Verb.PAXOS_PROPOSE_REQ, Verb.FAILURE_RSP, new ProposeVerbHandler());
|
||||
}
|
||||
|
||||
private void rejectRequestForTokenOutOfRange(Verb requestVerb, Verb responseVerb, AbstractPaxosVerbHandler handler) throws Exception
|
||||
{
|
||||
// reject a commit for a token the node neither owns nor is pending
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int key = 200;
|
||||
Commit commit = commit(key);
|
||||
handler.doVerb(Message.builder(requestVerb, commit).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, responseVerb, messageId, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptPrepareIfRejectionNotEnabled() throws Exception
|
||||
{
|
||||
acceptRequestIfRejectionNotEnabled(Verb.PAXOS_PREPARE_REQ, Verb.PAXOS_PREPARE_RSP, new PrepareVerbHandler());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptProposeIfRejectionNotEnabled() throws Exception
|
||||
{
|
||||
acceptRequestIfRejectionNotEnabled(Verb.PAXOS_PROPOSE_REQ, Verb.PAXOS_PROPOSE_RSP, new ProposeVerbHandler());
|
||||
}
|
||||
|
||||
private void acceptRequestIfRejectionNotEnabled(Verb requestVerb, Verb responseVerb, AbstractPaxosVerbHandler handler) throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
ListenableFuture<MessageDelivery> messageSink = registerOutgoingMessageSink();
|
||||
int messageId = randomInt();
|
||||
int key = 200;
|
||||
Commit commit = commit(key);
|
||||
handler.doVerb(Message.builder(requestVerb, commit).from(node1).withId(messageId).build());
|
||||
getAndVerifyResponse(messageSink, responseVerb, messageId, true, false);
|
||||
}
|
||||
|
||||
private void getAndVerifyResponse(ListenableFuture<MessageDelivery> messageSink,
|
||||
Verb verb,
|
||||
int messageId,
|
||||
boolean isOutOfRange,
|
||||
boolean expectFailure) throws InterruptedException, ExecutionException, TimeoutException
|
||||
{
|
||||
MessageDelivery response = messageSink.get(100, TimeUnit.MILLISECONDS);
|
||||
assertEquals(verb, response.message.verb());
|
||||
Assert.assertEquals(broadcastAddress, response.message.from());
|
||||
assertEquals(expectFailure, response.message.payload instanceof RequestFailureReason);
|
||||
assertEquals(messageId, response.message.id());
|
||||
Assert.assertEquals(node1, response.to);
|
||||
assertEquals(startingTotalMetricCount + (isOutOfRange ? 1 : 0), StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
assertEquals(startingKeyspaceMetricCount + (isOutOfRange ? 1 : 0), keyspaceMetricValue());
|
||||
}
|
||||
|
||||
private static Commit commit(int key)
|
||||
{
|
||||
TableMetadata tmd = Schema.instance.getTableMetadata(KEYSPACE, TABLE);
|
||||
return Commit.newPrepare(key(tmd, key), tmd, BallotGenerator.Global.nextBallot(Ballot.Flag.NONE));
|
||||
}
|
||||
|
||||
private static long keyspaceMetricValue()
|
||||
{
|
||||
return Keyspace.open(KEYSPACE).metric.outOfRangeTokenPaxosRequests.getCount();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,627 @@
|
|||
/*
|
||||
* 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.streaming;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import net.jpountz.lz4.LZ4Factory;
|
||||
import org.apache.cassandra.SchemaLoader;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.BufferDecoratedKey;
|
||||
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||
import org.apache.cassandra.db.DecoratedKey;
|
||||
import org.apache.cassandra.db.DeletionTime;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.db.SerializationHeader;
|
||||
import org.apache.cassandra.db.rows.Row;
|
||||
import org.apache.cassandra.db.rows.Rows;
|
||||
import org.apache.cassandra.db.streaming.CassandraStreamHeader;
|
||||
import org.apache.cassandra.db.streaming.CassandraStreamReader;
|
||||
import org.apache.cassandra.db.streaming.IStreamReader;
|
||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||
import org.apache.cassandra.io.sstable.SSTableMultiWriter;
|
||||
import org.apache.cassandra.io.sstable.SSTableSimpleIterator;
|
||||
import org.apache.cassandra.io.sstable.format.SSTableFormat;
|
||||
import org.apache.cassandra.io.sstable.format.SSTableReader;
|
||||
import org.apache.cassandra.io.sstable.format.Version;
|
||||
import org.apache.cassandra.io.sstable.format.big.BigFormat;
|
||||
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.TrackedDataInputPlus;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.metrics.StorageMetrics;
|
||||
import org.apache.cassandra.net.AsyncStreamingOutputPlus;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.streaming.async.StreamCompressionSerializer;
|
||||
import org.apache.cassandra.streaming.messages.StreamMessageHeader;
|
||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
import org.apache.cassandra.utils.TimeUUID;
|
||||
|
||||
import static org.apache.cassandra.streaming.StreamTestUtils.channelFactory;
|
||||
import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.broadcastAddress;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.node1;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.randomInt;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.setLocalTokens;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.setPendingRanges;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.token;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class StreamReaderTest
|
||||
{
|
||||
private static final String TEST_NAME = "streamreader_test_";
|
||||
private static final String KEYSPACE = TEST_NAME + "cql_keyspace";
|
||||
private static final String TABLE = "table1";
|
||||
|
||||
@BeforeClass
|
||||
public static void setupClass() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
|
||||
SchemaLoader.loadSchema();
|
||||
SchemaLoader.schemaDefinition(TEST_NAME);
|
||||
StorageService.instance.initServer(0);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setLogOutOfTokenRangeRequests(true);
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(true);
|
||||
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
// All tests suppose a 2 node ring, with the other peer having the tokens 0, 200, 400
|
||||
// Initially, the local node has no tokens so when indivividual test set owned tokens or
|
||||
// pending ranges for the local node, they're always in relation to this.
|
||||
// e.g. test calls setLocalTokens(100, 300) the ring now looks like
|
||||
// peer -> (min, 0], (100, 200], (300, 400]
|
||||
// local -> (0, 100], (200, 300], (400, max]
|
||||
//
|
||||
// Pending ranges are set in test using start/end pairs.
|
||||
// Ring is initialised:
|
||||
// peer -> (min, max]
|
||||
// local -> (,]
|
||||
// e.g. test calls setPendingRanges(0, 100, 200, 300)
|
||||
// the pending ranges for local would be calculated as:
|
||||
// local -> (0, 100], (200, 300]
|
||||
StorageService.instance.getTokenMetadata().updateNormalTokens(Lists.newArrayList(token(0),
|
||||
token(200),
|
||||
token(400)),
|
||||
node1);
|
||||
}
|
||||
@Test
|
||||
public void testReceiveWithNoOwnedRanges() throws Throwable
|
||||
{
|
||||
int[] tokens = {10, 20};
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSingleOwnedRangeReceivingTableWithRangeContained() throws Throwable
|
||||
{
|
||||
int[] tokens = {10, 20};
|
||||
setLocalTokens(100);
|
||||
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSingleOwnedRangeReceivedTableLowestKeyBoundsExclusive() throws Throwable
|
||||
{
|
||||
// verify that ranges are left exclusive
|
||||
int[] tokens = {0, 10};
|
||||
setLocalTokens(100);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSingleOwnedRangeReceivingTableRangeWithExactMatch() throws Throwable
|
||||
{
|
||||
// Because ranges are left exlusive, for the range (0, 100] the lowest permissable key is 1
|
||||
int[] tokens = {1, 100};
|
||||
setLocalTokens(100);
|
||||
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSingleOwnedRangeReceivingTableRangeLessThanOwned() throws Throwable
|
||||
{
|
||||
int[] tokens = {-100, 0};
|
||||
setLocalTokens(100);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSingleOwnedRangeReceivingTableRangeGreaterThanOwned() throws Throwable
|
||||
{
|
||||
int[] tokens = {101, 200};
|
||||
setLocalTokens(100);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSingleOwnedRangeReceivingTableRangeOverlappingOwned() throws Throwable
|
||||
{
|
||||
int[] tokens = {80, 120};
|
||||
setLocalTokens(100);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSingleOwnedWrappingRangeReceivingTableContainedBeforeMax() throws Throwable
|
||||
{
|
||||
int[] tokens = {110, 120};
|
||||
|
||||
// local node owns (min, 0] & (100, max], peer owns (0, 100]
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(token(0), broadcastAddress);
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(token(100), node1);
|
||||
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSingleOwnedWrappingRangeReceivingTableContainedAfterMin() throws Throwable
|
||||
{
|
||||
int[] tokens = {-150, -140};
|
||||
|
||||
// local node owns (min, 0] & (100, max], peer owns (0, 100]
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(token(0), broadcastAddress);
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(token(100), node1);
|
||||
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSingleOwnedWrappingRangeReceivingTableOverlappingUpward() throws Throwable
|
||||
{
|
||||
int[] tokens = {-10, 10};
|
||||
|
||||
// local node owns (min, 0] & (100, max], peer owns (0, 100]
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(token(0), broadcastAddress);
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(token(100), node1);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSingleOwnedWrappingRangeReceivingTableOverlappingDownward() throws Throwable
|
||||
{
|
||||
int[] tokens = {90, 110};
|
||||
|
||||
// local node owns (min, 0] & (100, max], peer owns (0, 100]
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(token(0), broadcastAddress);
|
||||
StorageService.instance.getTokenMetadata().updateNormalToken(token(100), node1);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithMultipleOwnedRangesReceivingTableRangeContainedInFirstOwned() throws Throwable
|
||||
{
|
||||
int[] tokens = {10, 20};
|
||||
setLocalTokens(100, 300, 500);
|
||||
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithMultipleOwnedRangesReceivingTableRangeContainedInLastOwned() throws Throwable
|
||||
{
|
||||
int[] tokens = {450, 460};
|
||||
setLocalTokens(100, 300, 500);
|
||||
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithMultipleOwnedRangesReceivingTableRangeLessThanOwned() throws Throwable
|
||||
{
|
||||
int[] tokens = {510, 520};
|
||||
setLocalTokens(100, 300, 500);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithMultipleOwnedRangesReceivingTableRangeGreaterThanOwned() throws Throwable
|
||||
{
|
||||
int[] tokens = {-20, -10};
|
||||
setLocalTokens(100, 300, 500);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithMultipleOwnedRangesReceivingTableRangeOverlappingOwned() throws Throwable
|
||||
{
|
||||
int[] tokens = {80, 120};
|
||||
setLocalTokens(100, 300, 500);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithMultipleOwnedRangesAllDisjointFromReceivingTableRange() throws Throwable
|
||||
{
|
||||
int[] tokens = {310, 320};
|
||||
setLocalTokens(100, 300, 500);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithMultipleOwnedRangesDisjointSpannedByReceivingTableRange() throws Throwable
|
||||
{
|
||||
int[] tokens = {80, 320};
|
||||
setLocalTokens(100, 300, 500);
|
||||
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithMultipleOwnedRangesReceivedTableRangeExactMatch() throws Throwable
|
||||
{
|
||||
// bacause ranges are left exclusive, for the range (200, 300] the lowest permissable key is 201
|
||||
int[] tokens = {201, 300};
|
||||
setLocalTokens(100, 300, 500);
|
||||
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithOwnedRangeWrappingAndReceivedFileWhollyContained() throws Throwable
|
||||
{
|
||||
// peer -> (-100, 0], (100, 200], (300, 400]
|
||||
// local -> (min, -100], (400, max]
|
||||
setLocalTokens(-100);
|
||||
int[] tokens = {-200, 500};
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithOwnedRangeWrappingAndReceivedFilePartiallyContained() throws Throwable
|
||||
{
|
||||
// peer -> (-100, 0], (100, 200], (300, 400]
|
||||
// local -> (min, -100], (400, max]
|
||||
setLocalTokens(-100);
|
||||
int[] tokens = {-200, 300};
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithOwnedRangeWrappingAndReceivedFileNotContained() throws Throwable
|
||||
{
|
||||
// peer -> (-100, 0], (100, 200], (300, 400]
|
||||
// local -> (min, -100], (400, max]
|
||||
setLocalTokens(-100);
|
||||
int[] tokens = {0, 300};
|
||||
tryReceiveExpectingFailure(tokens);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithSinglePendingRangeReceivingTableWithRangeContained() throws Throwable
|
||||
{
|
||||
int[] tokens = {10, 20};
|
||||
setPendingRanges(KEYSPACE, 0, 100);
|
||||
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveWithMultiplePendingRangesReceivingTableRangeContainedInFirstOwned() throws Throwable
|
||||
{
|
||||
int[] tokens = {10, 20};
|
||||
setPendingRanges(KEYSPACE, 0, 100, 200, 300, 400, 500);
|
||||
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveNormalizesOwnedAndPendingRanges() throws Throwable
|
||||
{
|
||||
// Incoming file is not covered by either a single owned or pending range,
|
||||
// but it is covered by the normalized set of both
|
||||
int[] tokens = {90, 110};
|
||||
setLocalTokens(100);
|
||||
setPendingRanges(KEYSPACE, 100, 120);
|
||||
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryReceiveExpectingSuccess(tokens, false);
|
||||
}
|
||||
|
||||
public static StreamSession setupStreamingSessionForTest()
|
||||
{
|
||||
StreamCoordinator streamCoordinator = new StreamCoordinator(StreamOperation.REPAIR, 1, channelFactory, false, false, null, PreviewKind.NONE);
|
||||
StreamResultFuture future = StreamResultFuture.createInitiator(nextTimeUUID(), StreamOperation.REPAIR, Collections.emptyList(), streamCoordinator);
|
||||
|
||||
InetAddressAndPort peer = FBUtilities.getBroadcastAddressAndPort();
|
||||
streamCoordinator.addSessionInfo(new SessionInfo(peer, 0, peer, Collections.emptyList(), Collections.emptyList(), StreamSession.State.INITIALIZED));
|
||||
|
||||
StreamSession session = streamCoordinator.getOrCreateOutboundSession(peer);
|
||||
session.init(future);
|
||||
return session;
|
||||
}
|
||||
|
||||
private static void tryReceiveExpectingSuccess(int[] tokens,
|
||||
boolean isOutOfRange) throws Throwable
|
||||
{
|
||||
StreamSession session = setupStreamingSessionForTest();
|
||||
StreamMessageHeader header = streamHeader();
|
||||
CassandraStreamHeader streamHeader = streamMessageHeader(tokens);
|
||||
long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount();
|
||||
IStreamReader reader = streamReader(header, streamHeader, session);
|
||||
StreamSummary streamSummary = new StreamSummary(streamHeader.tableId, 1, 0);
|
||||
session.prepareReceiving(streamSummary);
|
||||
try (SSTableMultiWriter ignored = reader.read(incomingStream(tokens)))
|
||||
{
|
||||
assertEquals(isOutOfRange, StorageMetrics.totalOpsForInvalidToken.getCount() > startMetricCount);
|
||||
}
|
||||
}
|
||||
|
||||
private static void tryReceiveExpectingFailure(int[] tokens) throws Throwable
|
||||
{
|
||||
StreamSession session = setupStreamingSessionForTest();
|
||||
StreamMessageHeader header = streamHeader();
|
||||
CassandraStreamHeader streamHeader = streamMessageHeader(tokens);
|
||||
long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount();
|
||||
StreamSummary streamSummary = new StreamSummary(streamHeader.tableId, 1, 0);
|
||||
session.prepareReceiving(streamSummary);
|
||||
try
|
||||
{
|
||||
IStreamReader reader = streamReader(header, streamHeader, session);
|
||||
try (SSTableMultiWriter ignored = reader.read(incomingStream(tokens)))
|
||||
{
|
||||
fail("Expected StreamReceivedOfTokenRangeException");
|
||||
}
|
||||
}
|
||||
catch (StreamReceivedOutOfTokenRangeException e)
|
||||
{
|
||||
// expected
|
||||
}
|
||||
assertTrue(StorageMetrics.totalOpsForInvalidToken.getCount() > startMetricCount);
|
||||
}
|
||||
|
||||
|
||||
private static class BufferSupplier implements AsyncStreamingOutputPlus.BufferSupplier
|
||||
{
|
||||
ByteBuffer supplied;
|
||||
|
||||
@Override
|
||||
public ByteBuffer get(int capacity)
|
||||
{
|
||||
supplied = ByteBuffer.allocateDirect(capacity);
|
||||
return supplied;
|
||||
}
|
||||
|
||||
public ByteBuffer getSupplied()
|
||||
{
|
||||
return supplied;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static DataInputPlus incomingStream(int...tokens) throws IOException
|
||||
{
|
||||
final net.jpountz.lz4.LZ4Compressor compressor = LZ4Factory.fastestInstance().fastCompressor();
|
||||
|
||||
DataOutputBuffer out = new DataOutputBuffer();
|
||||
for (int token : tokens)
|
||||
ByteBufferUtil.writeWithShortLength(ByteBufferUtil.bytes((long)token), out);
|
||||
|
||||
int current_version = MessagingService.current_version;
|
||||
|
||||
BufferSupplier bufferSupplier = new BufferSupplier();
|
||||
StreamCompressionSerializer.serialize(compressor, out.buffer(), current_version).write(bufferSupplier);
|
||||
|
||||
return new DataInputBuffer(bufferSupplier.getSupplied(), false);
|
||||
}
|
||||
|
||||
private static IStreamReader streamReader(StreamMessageHeader header, CassandraStreamHeader streamHeader, StreamSession session)
|
||||
{
|
||||
return new KeysOnlyStreamReader(header, streamHeader, session);
|
||||
}
|
||||
|
||||
private static StreamMessageHeader streamHeader()
|
||||
{
|
||||
TableMetadata tmd = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE).metadata();
|
||||
int fakeSession = randomInt(9);
|
||||
int fakeSeq = randomInt(9);
|
||||
return new StreamMessageHeader(tmd.id,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
fakeSession,
|
||||
fakeSeq,
|
||||
System.currentTimeMillis(),
|
||||
null);
|
||||
}
|
||||
|
||||
private static CassandraStreamHeader streamMessageHeader(int...tokens)
|
||||
{
|
||||
TableMetadata tmd = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE).metadata();
|
||||
Version version = BigFormat.latestVersion;
|
||||
List<SSTableReader.PartitionPositionBounds> fakeSections = new ArrayList<>();
|
||||
// each decorated key takes up (2 + 8) bytes, so this enables the
|
||||
// StreamReader to calculate the expected number of bytes to read
|
||||
fakeSections.add(new SSTableReader.PartitionPositionBounds(0L, (tokens.length * 10L) - 1));
|
||||
|
||||
return CassandraStreamHeader.builder()
|
||||
.withTableId(tmd.id)
|
||||
.withSerializationHeader(SerializationHeader.makeWithoutStats(tmd).toComponent())
|
||||
.withSSTableFormat(SSTableFormat.Type.BIG)
|
||||
.withSSTableVersion(version)
|
||||
.withSections(fakeSections)
|
||||
.build();
|
||||
}
|
||||
|
||||
// Simplifies generating test data as token == key (expects key to be an encoded long)
|
||||
private static class FakeMurmur3Partitioner extends Murmur3Partitioner
|
||||
{
|
||||
public DecoratedKey decorateKey(ByteBuffer key)
|
||||
{
|
||||
return new BufferDecoratedKey(new LongToken(ByteBufferUtil.toLong(key)), key);
|
||||
}
|
||||
}
|
||||
|
||||
// Stream reader which no-ops the reading/writing of the actual partition data.
|
||||
// As we only care about keys/tokens here, we don't need to generate the rest
|
||||
// of the sstable data to simulate a stream
|
||||
private static class KeysOnlyStreamReader extends CassandraStreamReader
|
||||
{
|
||||
public KeysOnlyStreamReader(StreamMessageHeader header, CassandraStreamHeader streamHeader, StreamSession session)
|
||||
{
|
||||
super(header, streamHeader, session);
|
||||
}
|
||||
|
||||
protected SSTableMultiWriter createWriter(ColumnFamilyStore cfs, long totalSize, long repairedAt, TimeUUID pendingRepair, SSTableFormat.Type format) throws IOException
|
||||
{
|
||||
return super.createWriter(cfs, totalSize, repairedAt, pendingRepair, format);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StreamDeserializer getDeserializer(TableMetadata metadata, TrackedDataInputPlus in, Version inputVersion, StreamSession session, SSTableMultiWriter writer) throws IOException
|
||||
{
|
||||
return new TestStreamDeserializer(metadata, in, inputVersion, getHeader(metadata), session, writer);
|
||||
}
|
||||
|
||||
private static class TestStreamDeserializer extends CassandraStreamReader.StreamDeserializer
|
||||
{
|
||||
TestStreamDeserializer(TableMetadata metadata, DataInputPlus in, Version version, SerializationHeader header, StreamSession session, SSTableMultiWriter writer) throws IOException
|
||||
{
|
||||
super(metadata.unbuild().partitioner(new FakeMurmur3Partitioner()).build(), in, version, header, session, writer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readPartition()
|
||||
{
|
||||
// no-op, our dummy stream contains only decorated keys
|
||||
partitionLevelDeletion = DeletionTime.LIVE;
|
||||
iterator = new SSTableSimpleIterator.EmptySSTableSimpleIterator(metadata());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Row staticRow()
|
||||
{
|
||||
return Rows.EMPTY_STATIC_ROW;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeletionTime partitionLevelDeletion()
|
||||
{
|
||||
return DeletionTime.LIVE;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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.streaming;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.SchemaLoader;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.locator.RangesAtEndpoint;
|
||||
import org.apache.cassandra.metrics.StorageMetrics;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.streaming.messages.StreamMessage;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import static org.apache.cassandra.streaming.StreamTestUtils.session;
|
||||
import static org.apache.cassandra.streaming.messages.StreamMessage.Type.COMPLETE;
|
||||
import static org.apache.cassandra.streaming.messages.StreamMessage.Type.PREPARE_SYNACK;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.generateRangesAtEndpoint;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.node1;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.setLocalTokens;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.token;
|
||||
|
||||
public class StreamSessionOwnedRangesTest
|
||||
{
|
||||
private static final String TEST_NAME = "strmsn_owned_ranges_test_";
|
||||
private static final String KEYSPACE = TEST_NAME + "cql_keyspace";
|
||||
private static final String TABLE = "table1";
|
||||
|
||||
@BeforeClass
|
||||
public static void setupClass() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
|
||||
SchemaLoader.loadSchema();
|
||||
SchemaLoader.schemaDefinition(TEST_NAME);
|
||||
StorageService.instance.initServer(0);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception
|
||||
{
|
||||
DatabaseDescriptor.setLogOutOfTokenRangeRequests(true);
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(true);
|
||||
|
||||
StorageService.instance.getTokenMetadata().clearUnsafe();
|
||||
// All tests suppose a 2 node ring, with the other peer having the tokens 0, 200, 300
|
||||
// Initially, the local node has no tokens so when indivividual test set owned tokens or
|
||||
// pending ranges for the local node, they're always in relation to this.
|
||||
// e.g. test calls setLocalTokens(100, 300) the ring now looks like
|
||||
// peer -> (min, 0], (100, 200], (300, 400]
|
||||
// local -> (0, 100], (200, 300], (400, max]
|
||||
//
|
||||
// Pending ranges are set in test using start/end pairs.
|
||||
// Ring is initialised:
|
||||
// peer -> (min, max]
|
||||
// local -> (,]
|
||||
// e.g. test calls setPendingRanges(0, 100, 200, 300)
|
||||
// the pending ranges for local would be calculated as:
|
||||
// local -> (0, 100], (200, 300]
|
||||
StorageService.instance.getTokenMetadata().updateNormalTokens(Lists.newArrayList(token(0),
|
||||
token(200),
|
||||
token(400)),
|
||||
node1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrepareWithAllRequestedRangesWithinOwned()
|
||||
{
|
||||
setLocalTokens(100);
|
||||
InetAddressAndPort endpoint = FBUtilities.getBroadcastAddressAndPort();
|
||||
Collection<StreamRequest> requests = streamRequests(generateRangesAtEndpoint(endpoint, 0, 10, 70, 80),
|
||||
RangesAtEndpoint.empty(endpoint));
|
||||
|
||||
// prepare request should succeed with or without rejection enabled
|
||||
tryPrepareExpectingSuccess(requests, false);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryPrepareExpectingSuccess(requests, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrepareWithAllRequestedRangesOutsideOwned()
|
||||
{
|
||||
setLocalTokens(100);
|
||||
InetAddressAndPort endpoint = FBUtilities.getBroadcastAddressAndPort();
|
||||
|
||||
Collection<StreamRequest> requests = streamRequests(generateRangesAtEndpoint(endpoint, -20, -10, 110, 120, 310, 320),
|
||||
RangesAtEndpoint.empty(endpoint));
|
||||
|
||||
tryPrepareExpectingFailure(requests);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryPrepareExpectingSuccess(requests, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrepareWithSomeRequestedRangesOutsideOwned()
|
||||
{
|
||||
setLocalTokens(100);
|
||||
InetAddressAndPort endpoint = FBUtilities.getBroadcastAddressAndPort();
|
||||
|
||||
Collection<StreamRequest> requests = streamRequests(generateRangesAtEndpoint(endpoint, -20, -10, 30, 40, 310, 320),
|
||||
RangesAtEndpoint.empty(endpoint));
|
||||
|
||||
tryPrepareExpectingFailure(requests);
|
||||
|
||||
DatabaseDescriptor.setRejectOutOfTokenRangeRequests(false);
|
||||
tryPrepareExpectingSuccess(requests, true);
|
||||
}
|
||||
|
||||
private static void tryPrepareExpectingSuccess(Collection<StreamRequest> requests, boolean isOutOfRange)
|
||||
{
|
||||
final List<StreamMessage> sent = new ArrayList<>();
|
||||
StreamSession session = session(sent);
|
||||
|
||||
sent.clear();
|
||||
long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount();
|
||||
|
||||
session.state(StreamSession.State.PREPARING);
|
||||
session.prepareAsync(requests, Collections.emptySet());
|
||||
|
||||
assertEquals(2, sent.size());
|
||||
assertEquals(PREPARE_SYNACK, sent.get(0).type);
|
||||
assertEquals(COMPLETE, sent.get(1).type);
|
||||
|
||||
assertEquals(startMetricCount + (isOutOfRange ? 1 : 0), StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
}
|
||||
|
||||
private static void tryPrepareExpectingFailure(Collection<StreamRequest> requests)
|
||||
{
|
||||
final List<StreamMessage> sent = new ArrayList<>();
|
||||
StreamSession session = session(sent);
|
||||
sent.clear();
|
||||
long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount();
|
||||
try
|
||||
{
|
||||
session.state(StreamSession.State.PREPARING);
|
||||
session.prepareAsync(requests, Collections.emptySet());
|
||||
fail("Expected StreamRequestOfTokenRangeException");
|
||||
}
|
||||
catch (StreamRequestOutOfTokenRangeException e)
|
||||
{
|
||||
// expected
|
||||
}
|
||||
assertTrue(sent.isEmpty());
|
||||
assertEquals(startMetricCount + 1, StorageMetrics.totalOpsForInvalidToken.getCount());
|
||||
}
|
||||
|
||||
private static Collection<StreamRequest> streamRequests(RangesAtEndpoint fullRanges,
|
||||
RangesAtEndpoint transientRanges)
|
||||
{
|
||||
return Collections.singleton(new StreamRequest(KEYSPACE,
|
||||
fullRanges,
|
||||
transientRanges,
|
||||
Collections.singleton(TABLE)));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.streaming;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.netty.util.concurrent.Future;
|
||||
import org.apache.cassandra.streaming.messages.StreamMessage;
|
||||
import org.apache.cassandra.utils.concurrent.AsyncPromise;
|
||||
|
||||
import static org.apache.cassandra.net.MessagingService.current_version;
|
||||
import static org.apache.cassandra.streaming.StreamingChannel.Factory.Global.streamingFactory;
|
||||
import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
|
||||
import static org.apache.cassandra.utils.TokenRangeTestUtil.node1;
|
||||
|
||||
public class StreamTestUtils
|
||||
{
|
||||
static StreamSession session(List<StreamMessage> sentMessages)
|
||||
{
|
||||
StreamCoordinator streamCoordinator = new StreamCoordinator(StreamOperation.REPAIR, 1, streamingFactory(), false, false, null, PreviewKind.NONE);
|
||||
StreamResultFuture future = StreamResultFuture.createInitiator(nextTimeUUID(), StreamOperation.REPAIR, Collections.<StreamEventHandler>emptyList(), streamCoordinator);
|
||||
|
||||
StreamSession session = new StreamSession(StreamOperation.REPAIR,
|
||||
node1,
|
||||
channelFactory,
|
||||
null,
|
||||
current_version,
|
||||
true,
|
||||
0,
|
||||
null,
|
||||
PreviewKind.NONE)
|
||||
{
|
||||
@Override
|
||||
public void progress(String filename, ProgressInfo.Direction direction, long bytes, long delta, long total)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Future<?> sendControlMessage(StreamMessage message) {
|
||||
sentMessages.add(message);
|
||||
final AsyncPromise<Object> promise = new AsyncPromise<>(null);
|
||||
promise.setSuccess(null);
|
||||
return promise;
|
||||
}
|
||||
|
||||
};
|
||||
session.init(future);
|
||||
return session;
|
||||
}
|
||||
|
||||
static StreamingChannel.Factory channelFactory = (InetSocketAddress to, int messagingVersion, StreamingChannel.Kind kind) -> {
|
||||
throw new UnsupportedOperationException();
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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.utils;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
import org.apache.cassandra.dht.ByteOrderedPartitioner;
|
||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||
import org.apache.cassandra.dht.Range;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.locator.RangesAtEndpoint;
|
||||
import org.apache.cassandra.locator.Replica;
|
||||
import org.apache.cassandra.net.Message;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.net.Verb;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
|
||||
import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
|
||||
|
||||
public class TokenRangeTestUtil
|
||||
{
|
||||
public static final InetAddressAndPort node1;
|
||||
public static final InetAddressAndPort broadcastAddress;
|
||||
|
||||
static
|
||||
{
|
||||
try
|
||||
{
|
||||
node1 = InetAddressAndPort.getByName("127.0.1.99");
|
||||
}
|
||||
catch (UnknownHostException e)
|
||||
{
|
||||
throw new RuntimeException("Error initializing InetAddressAndPort");
|
||||
}
|
||||
broadcastAddress = FBUtilities.getBroadcastAddressAndPort();
|
||||
}
|
||||
|
||||
public static TimeUUID uuid()
|
||||
{
|
||||
return nextTimeUUID();
|
||||
}
|
||||
|
||||
public static void setPendingRanges(String keyspace, int... tokens)
|
||||
{
|
||||
Multimap<Range<Token>, Replica> pending = HashMultimap.create();
|
||||
for (Range<Token> range : generateRanges(tokens))
|
||||
pending.put(range, new Replica(broadcastAddress, range, true));
|
||||
|
||||
StorageService.instance.getTokenMetadata().setPendingRangesUnsafe(keyspace, pending);
|
||||
}
|
||||
|
||||
public static void setLocalTokens(int... tokens)
|
||||
{
|
||||
List<Token> tokenList = new ArrayList<>();
|
||||
for (int token : tokens)
|
||||
tokenList.add(token(token));
|
||||
StorageService.instance.getTokenMetadata().updateNormalTokens(tokenList, broadcastAddress);
|
||||
}
|
||||
|
||||
private static final Random random = new Random();
|
||||
public static int randomInt()
|
||||
{
|
||||
return randomInt(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static int randomInt(int max)
|
||||
{
|
||||
return random.nextInt(max);
|
||||
}
|
||||
|
||||
public static RangesAtEndpoint generateRangesAtEndpoint(InetAddressAndPort endpoint, int... rangePairs)
|
||||
{
|
||||
if (rangePairs.length % 2 == 1)
|
||||
throw new RuntimeException("generateRangesAtEndpoint argument count should be even");
|
||||
|
||||
RangesAtEndpoint.Builder builder = RangesAtEndpoint.builder(endpoint);
|
||||
|
||||
for (int i = 0; i < rangePairs.length; i += 2)
|
||||
{
|
||||
builder.add(Replica.fullReplica(endpoint, generateRange(rangePairs[i], rangePairs[i + 1])));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static List<Range<Token>> generateRanges(int... rangePairs)
|
||||
{
|
||||
if (rangePairs.length % 2 == 1)
|
||||
throw new RuntimeException("generateRanges argument count should be even");
|
||||
|
||||
List<Range<Token>> ranges = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < rangePairs.length; i += 2)
|
||||
{
|
||||
ranges.add(generateRange(rangePairs[i], rangePairs[i + 1]));
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
public static Range<Token> generateRange(int left, int right)
|
||||
{
|
||||
return new Range<>(token(left), token(right));
|
||||
}
|
||||
|
||||
public static Token token(int token)
|
||||
{
|
||||
return new Murmur3Partitioner.LongToken(token);
|
||||
}
|
||||
|
||||
public static Token bytesToken(int token)
|
||||
{
|
||||
return new ByteOrderedPartitioner.BytesToken(ByteBufferUtil.bytes(token));
|
||||
}
|
||||
|
||||
public static ListenableFuture<MessageDelivery> registerOutgoingMessageSink()
|
||||
{
|
||||
return registerOutgoingMessageSink(true);
|
||||
}
|
||||
|
||||
public static ListenableFuture<MessageDelivery> registerOutgoingMessageSink(boolean dropFirstRepairRsp)
|
||||
{
|
||||
final SettableFuture<MessageDelivery> future = SettableFuture.create();
|
||||
|
||||
MessagingService.instance().outboundSink.clear();
|
||||
MessagingService.instance().outboundSink.add((Message<?> message, InetAddressAndPort to) ->
|
||||
{
|
||||
// Pots-CASSANDRA-17613 repair always returns with an immediate REPAIR_RSP
|
||||
if (dropFirstRepairRsp && message.verb() == Verb.REPAIR_RSP)
|
||||
return true;
|
||||
future.set(new MessageDelivery(message, to));
|
||||
return true;
|
||||
});
|
||||
|
||||
MessagingService.instance().inboundSink.clear();
|
||||
MessagingService.instance().inboundSink.add((Message<?> message) -> false);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public static class MessageDelivery
|
||||
{
|
||||
public final Message<?> message;
|
||||
public final InetAddressAndPort to;
|
||||
|
||||
MessageDelivery(Message<?> message, InetAddressAndPort to)
|
||||
{
|
||||
this.message = message;
|
||||
this.to = to;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue