Restore resumable hints delivery

Backport of CASSANDRA-11960

patch by Tommy Stendahl, Stefan Podkowinski and Branimir Lambov; reviewed by Aleksey Yeschenko for CASSANDRA-14419
This commit is contained in:
tommy stendahl 2018-07-03 12:53:59 +02:00 committed by Aleksey Yeshchenko
parent 1e478d380b
commit c4982587bf
12 changed files with 511 additions and 64 deletions

View File

@ -1,4 +1,5 @@
3.0.17
* Restore resumable hints delivery, backport CASSANDRA-11960 (CASSANDRA-14419)
* Always close RT markers returned by ReadCommand#executeLocally() (CASSANDRA-14515)
* Reverse order queries with range tombstones can cause data loss (CASSANDRA-14513)
* Fix regression of lagging commitlog flush log message (CASSANDRA-14451)

View File

@ -25,17 +25,18 @@ import java.util.zip.CRC32;
import org.apache.cassandra.io.util.ChannelProxy;
import org.apache.cassandra.io.util.DataPosition;
import org.apache.cassandra.io.util.RandomAccessReader;
import org.apache.cassandra.utils.NativeLibrary;
/**
* A {@link RandomAccessReader} wrapper that calctulates the CRC in place.
* A {@link RandomAccessReader} wrapper that calculates the CRC in place.
*
* Useful for {@link org.apache.cassandra.hints.HintsReader}, for example, where we must verify the CRC, yet don't want
* to allocate an extra byte array just that purpose. The CRC can be embedded in the input stream and checked via checkCrc().
*
* In addition to calculating the CRC, it allows to enforce a maximim known size. This is needed
* In addition to calculating the CRC, it allows to enforce a maximum known size. This is needed
* so that {@link org.apache.cassandra.db.Mutation.MutationSerializer} doesn't blow up the heap when deserializing a
* corrupted sequence by reading a huge corrupted length of bytes via
* via {@link org.apache.cassandra.utils.ByteBufferUtil#readWithLength(java.io.DataInput)}.
* {@link org.apache.cassandra.utils.ByteBufferUtil#readWithLength(java.io.DataInput)}.
*/
public class ChecksummedDataInput extends RandomAccessReader.RandomAccessReaderWithOwnChannel
{
@ -63,9 +64,37 @@ public class ChecksummedDataInput extends RandomAccessReader.RandomAccessReaderW
return new Builder(new ChannelProxy(file)).build();
}
protected void releaseBuffer()
static class Position implements InputPosition
{
super.releaseBuffer();
final long sourcePosition;
public Position(long sourcePosition)
{
super();
this.sourcePosition = sourcePosition;
}
@Override
public long subtract(InputPosition other)
{
return sourcePosition - ((Position)other).sourcePosition;
}
}
/**
* Return a seekable representation of the current position. For compressed files this is chunk position
* in file and offset within chunk.
*/
public InputPosition getSeekPosition()
{
return new Position(getPosition());
}
public void seek(InputPosition pos)
{
updateCrc();
bufferOffset = ((Position) pos).sourcePosition;
buffer.position(0).limit(0);
}
public void resetCrc()
@ -80,6 +109,15 @@ public class ChecksummedDataInput extends RandomAccessReader.RandomAccessReaderW
limitMark = mark();
}
/**
* Returns the position in the source file, which is different for getPosition() for compressed/encrypted files
* and may be imprecise.
*/
protected long getSourcePosition()
{
return bufferOffset;
}
public void resetLimit()
{
limit = Long.MAX_VALUE;
@ -141,6 +179,11 @@ public class ChecksummedDataInput extends RandomAccessReader.RandomAccessReaderW
crcPosition = buffer.position();
}
public void tryUncacheRead()
{
NativeLibrary.trySkipCache(getChannel().getFileDescriptor(), 0, getSourcePosition(), getPath());
}
private void updateCrc()
{
if (crcPosition == buffer.position() || crcUpdateDisabled)

View File

@ -29,7 +29,8 @@ import org.apache.cassandra.utils.memory.BufferPool;
public final class CompressedChecksummedDataInput extends ChecksummedDataInput
{
private final ICompressor compressor;
private volatile long filePosition = 0;
private volatile long filePosition = 0; // Current position in file, advanced when reading chunk.
private volatile long sourcePosition = 0; // Current position in file to report, advanced after consuming chunk.
private volatile ByteBuffer compressedBuffer = null;
private final ByteBuffer metadataBuffer = ByteBuffer.allocate(CompressedHintsWriter.METADATA_SIZE);
@ -39,7 +40,7 @@ public final class CompressedChecksummedDataInput extends ChecksummedDataInput
assert regions == null; //mmapped regions are not supported
compressor = builder.compressor;
filePosition = builder.position;
sourcePosition = filePosition = builder.position;
}
/**
@ -51,8 +52,56 @@ public final class CompressedChecksummedDataInput extends ChecksummedDataInput
return filePosition == channel.size() && buffer.remaining() == 0;
}
public long getSourcePosition()
{
return sourcePosition;
}
static class Position extends ChecksummedDataInput.Position
{
final long bufferStart;
final int bufferPosition;
public Position(long sourcePosition, long bufferStart, int bufferPosition)
{
super(sourcePosition);
this.bufferStart = bufferStart;
this.bufferPosition = bufferPosition;
}
@Override
public long subtract(InputPosition o)
{
Position other = (Position) o;
return bufferStart - other.bufferStart + bufferPosition - other.bufferPosition;
}
}
public InputPosition getSeekPosition()
{
return new Position(sourcePosition, bufferOffset, buffer.position());
}
public void seek(InputPosition p)
{
Position pos = (Position) p;
bufferOffset = pos.bufferStart;
filePosition = pos.sourcePosition;
buffer.position(0).limit(0);
resetCrc();
reBuffer();
buffer.position(pos.bufferPosition);
assert sourcePosition == pos.sourcePosition;
assert bufferOffset == pos.bufferStart;
assert buffer.position() == pos.bufferPosition;
}
protected void reBufferStandard()
{
sourcePosition = filePosition;
if (isEOF())
return;
metadataBuffer.clear();
channel.read(metadataBuffer, filePosition);
filePosition += CompressedHintsWriter.METADATA_SIZE;

View File

@ -23,6 +23,8 @@ import java.util.Map;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Supplier;
import com.google.common.util.concurrent.RateLimiter;
@ -48,12 +50,14 @@ final class HintsDispatchExecutor
private final File hintsDirectory;
private final ExecutorService executor;
private final AtomicBoolean isPaused;
private final Function<InetAddress, Boolean> isAlive;
private final Map<UUID, Future> scheduledDispatches;
HintsDispatchExecutor(File hintsDirectory, int maxThreads, AtomicBoolean isPaused)
HintsDispatchExecutor(File hintsDirectory, int maxThreads, AtomicBoolean isPaused, Function<InetAddress, Boolean> isAlive)
{
this.hintsDirectory = hintsDirectory;
this.isPaused = isPaused;
this.isAlive = isAlive;
scheduledDispatches = new ConcurrentHashMap<>();
executor = new JMXEnabledThreadPoolExecutor(maxThreads, 1, TimeUnit.MINUTES,
@ -69,6 +73,14 @@ final class HintsDispatchExecutor
{
scheduledDispatches.clear();
executor.shutdownNow();
try
{
executor.awaitTermination(1, TimeUnit.MINUTES);
}
catch (InterruptedException e)
{
throw new AssertionError(e);
}
}
boolean isScheduled(HintsStore store)
@ -255,9 +267,10 @@ final class HintsDispatchExecutor
private boolean deliver(HintsDescriptor descriptor, InetAddress address)
{
File file = new File(hintsDirectory, descriptor.fileName());
Long offset = store.getDispatchOffset(descriptor).orElse(null);
InputPosition offset = store.getDispatchOffset(descriptor);
try (HintsDispatcher dispatcher = HintsDispatcher.create(file, rateLimiter, address, descriptor.hostId, isPaused))
BooleanSupplier shouldAbort = () -> !isAlive.apply(address) || isPaused.get();
try (HintsDispatcher dispatcher = HintsDispatcher.create(file, rateLimiter, address, descriptor.hostId, shouldAbort))
{
if (offset != null)
dispatcher.seek(offset);
@ -271,7 +284,7 @@ final class HintsDispatchExecutor
}
else
{
store.markDispatchOffset(descriptor, dispatcher.dispatchOffset());
store.markDispatchOffset(descriptor, dispatcher.dispatchPosition());
store.offerFirst(descriptor);
logger.info("Finished hinted handoff of file {} to endpoint {}: {}, partially", descriptor.fileName(), address, hostId);
return false;

View File

@ -22,7 +22,7 @@ import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import com.google.common.util.concurrent.RateLimiter;
@ -30,7 +30,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.net.IAsyncCallbackWithFailure;
import org.apache.cassandra.net.MessageIn;
import org.apache.cassandra.net.MessagingService;
@ -52,25 +51,25 @@ final class HintsDispatcher implements AutoCloseable
private final UUID hostId;
private final InetAddress address;
private final int messagingVersion;
private final AtomicBoolean isPaused;
private final BooleanSupplier abortRequested;
private long currentPageOffset;
private InputPosition currentPagePosition;
private HintsDispatcher(HintsReader reader, UUID hostId, InetAddress address, int messagingVersion, AtomicBoolean isPaused)
private HintsDispatcher(HintsReader reader, UUID hostId, InetAddress address, int messagingVersion, BooleanSupplier abortRequested)
{
currentPageOffset = 0L;
currentPagePosition = null;
this.reader = reader;
this.hostId = hostId;
this.address = address;
this.messagingVersion = messagingVersion;
this.isPaused = isPaused;
this.abortRequested = abortRequested;
}
static HintsDispatcher create(File file, RateLimiter rateLimiter, InetAddress address, UUID hostId, AtomicBoolean isPaused)
static HintsDispatcher create(File file, RateLimiter rateLimiter, InetAddress address, UUID hostId, BooleanSupplier abortRequested)
{
int messagingVersion = MessagingService.instance().getVersion(address);
return new HintsDispatcher(HintsReader.open(file, rateLimiter), hostId, address, messagingVersion, isPaused);
return new HintsDispatcher(HintsReader.open(file, rateLimiter), hostId, address, messagingVersion, abortRequested);
}
public void close()
@ -78,10 +77,9 @@ final class HintsDispatcher implements AutoCloseable
reader.close();
}
void seek(long bytes)
void seek(InputPosition position)
{
reader.seek(bytes);
currentPageOffset = 0L;
reader.seek(position);
}
/**
@ -91,7 +89,7 @@ final class HintsDispatcher implements AutoCloseable
{
for (HintsReader.Page page : reader)
{
currentPageOffset = page.offset;
currentPagePosition = page.position;
if (dispatch(page) != Action.CONTINUE)
return false;
}
@ -102,20 +100,11 @@ final class HintsDispatcher implements AutoCloseable
/**
* @return offset of the first non-delivered page
*/
long dispatchOffset()
InputPosition dispatchPosition()
{
return currentPageOffset;
return currentPagePosition;
}
private boolean isHostAlive()
{
return FailureDetector.instance.isAlive(address);
}
private boolean isPaused()
{
return isPaused.get();
}
// retry in case of a timeout; stop in case of a failure, host going down, or delivery paused
private Action dispatch(HintsReader.Page page)
@ -156,7 +145,7 @@ final class HintsDispatcher implements AutoCloseable
{
while (hints.hasNext())
{
if (!isHostAlive() || isPaused())
if (abortRequested.getAsBoolean())
return Action.ABORT;
callbacks.add(sendFunction.apply(hints.next()));
}

View File

@ -109,7 +109,7 @@ class HintsReader implements AutoCloseable, Iterable<HintsReader.Page>
return descriptor;
}
void seek(long newPosition)
void seek(InputPosition newPosition)
{
input.seek(newPosition);
}
@ -126,21 +126,21 @@ class HintsReader implements AutoCloseable, Iterable<HintsReader.Page>
final class Page
{
public final long offset;
public final InputPosition position;
private Page(long offset)
private Page(InputPosition inputPosition)
{
this.offset = offset;
this.position = inputPosition;
}
Iterator<Hint> hintsIterator()
{
return new HintsIterator(offset);
return new HintsIterator(position);
}
Iterator<ByteBuffer> buffersIterator()
{
return new BuffersIterator(offset);
return new BuffersIterator(position);
}
}
@ -149,12 +149,12 @@ class HintsReader implements AutoCloseable, Iterable<HintsReader.Page>
@SuppressWarnings("resource")
protected Page computeNext()
{
NativeLibrary.trySkipCache(input.getChannel().getFileDescriptor(), 0, input.getFilePointer(), input.getPath());
input.tryUncacheRead();
if (input.isEOF())
return endOfData();
return new Page(input.getFilePointer());
return new Page(input.getSeekPosition());
}
}
@ -163,9 +163,9 @@ class HintsReader implements AutoCloseable, Iterable<HintsReader.Page>
*/
final class HintsIterator extends AbstractIterator<Hint>
{
private final long offset;
private final InputPosition offset;
HintsIterator(long offset)
HintsIterator(InputPosition offset)
{
super();
this.offset = offset;
@ -177,12 +177,12 @@ class HintsReader implements AutoCloseable, Iterable<HintsReader.Page>
do
{
long position = input.getFilePointer();
InputPosition position = input.getSeekPosition();
if (input.isEOF())
return endOfData(); // reached EOF
if (position - offset >= PAGE_SIZE)
if (position.subtract(offset) >= PAGE_SIZE)
return endOfData(); // read page size or more bytes
try
@ -260,9 +260,9 @@ class HintsReader implements AutoCloseable, Iterable<HintsReader.Page>
*/
final class BuffersIterator extends AbstractIterator<ByteBuffer>
{
private final long offset;
private final InputPosition offset;
BuffersIterator(long offset)
BuffersIterator(InputPosition offset)
{
super();
this.offset = offset;
@ -274,12 +274,12 @@ class HintsReader implements AutoCloseable, Iterable<HintsReader.Page>
do
{
long position = input.getFilePointer();
InputPosition position = input.getSeekPosition();
if (input.isEOF())
return endOfData(); // reached EOF
if (position - offset >= PAGE_SIZE)
if (position.subtract(offset) >= PAGE_SIZE)
return endOfData(); // read page size or more bytes
try

View File

@ -30,6 +30,7 @@ import java.util.function.Supplier;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -37,6 +38,8 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.gms.IFailureDetector;
import org.apache.cassandra.metrics.HintedHandoffMetrics;
import org.apache.cassandra.metrics.StorageMetrics;
import org.apache.cassandra.dht.Token;
@ -60,7 +63,7 @@ public final class HintsService implements HintsServiceMBean
{
private static final Logger logger = LoggerFactory.getLogger(HintsService.class);
public static final HintsService instance = new HintsService();
public static HintsService instance = new HintsService();
private static final String MBEAN_NAME = "org.apache.cassandra.hints:type=HintsService";
@ -81,6 +84,12 @@ public final class HintsService implements HintsServiceMBean
public final HintedHandoffMetrics metrics;
private HintsService()
{
this(FailureDetector.instance);
}
@VisibleForTesting
HintsService(IFailureDetector failureDetector)
{
File hintsDirectory = DatabaseDescriptor.getHintsDirectory();
int maxDeliveryThreads = DatabaseDescriptor.getMaxHintsDeliveryThreads();
@ -92,7 +101,7 @@ public final class HintsService implements HintsServiceMBean
bufferPool = new HintsBufferPool(bufferSize, writeExecutor::flushBuffer);
isDispatchPaused = new AtomicBoolean(true);
dispatchExecutor = new HintsDispatchExecutor(hintsDirectory, maxDeliveryThreads, isDispatchPaused);
dispatchExecutor = new HintsDispatchExecutor(hintsDirectory, maxDeliveryThreads, isDispatchPaused, failureDetector::isAlive);
// periodically empty the current content of the buffers
int flushPeriod = DatabaseDescriptor.getHintsFlushPeriodInMS();
@ -225,7 +234,7 @@ public final class HintsService implements HintsServiceMBean
* Will abort dispatch sessions that are currently in progress (which is okay, it's idempotent),
* and make sure the buffers are flushed, hints files written and fsynced.
*/
public synchronized void shutdownBlocking()
public synchronized void shutdownBlocking() throws ExecutionException, InterruptedException
{
if (isShutDown)
throw new IllegalStateException("HintsService has already been shut down");
@ -237,8 +246,8 @@ public final class HintsService implements HintsServiceMBean
triggerFlushingFuture.cancel(false);
writeExecutor.flushBufferPool(bufferPool);
writeExecutor.closeAllWriters();
writeExecutor.flushBufferPool(bufferPool).get();
writeExecutor.closeAllWriters().get();
dispatchExecutor.shutdownBlocking();
writeExecutor.shutdownBlocking();
@ -370,4 +379,12 @@ public final class HintsService implements HintsServiceMBean
{
return catalog;
}
/**
* Returns true in case service is shut down.
*/
public boolean isShutDown()
{
return isShutDown;
}
}

View File

@ -50,7 +50,7 @@ final class HintsStore
private final File hintsDirectory;
private final ImmutableMap<String, Object> writerParams;
private final Map<HintsDescriptor, Long> dispatchOffsets;
private final Map<HintsDescriptor, InputPosition> dispatchPositions;
private final Deque<HintsDescriptor> dispatchDequeue;
private final Queue<HintsDescriptor> blacklistedFiles;
@ -64,7 +64,7 @@ final class HintsStore
this.hintsDirectory = hintsDirectory;
this.writerParams = writerParams;
dispatchOffsets = new ConcurrentHashMap<>();
dispatchPositions = new ConcurrentHashMap<>();
dispatchDequeue = new ConcurrentLinkedDeque<>(descriptors);
blacklistedFiles = new ConcurrentLinkedQueue<>();
@ -143,19 +143,19 @@ final class HintsStore
return !dispatchDequeue.isEmpty();
}
Optional<Long> getDispatchOffset(HintsDescriptor descriptor)
InputPosition getDispatchOffset(HintsDescriptor descriptor)
{
return Optional.ofNullable(dispatchOffsets.get(descriptor));
return dispatchPositions.get(descriptor);
}
void markDispatchOffset(HintsDescriptor descriptor, long mark)
void markDispatchOffset(HintsDescriptor descriptor, InputPosition inputPosition)
{
dispatchOffsets.put(descriptor, mark);
dispatchPositions.put(descriptor, inputPosition);
}
void cleanUp(HintsDescriptor descriptor)
{
dispatchOffsets.remove(descriptor);
dispatchPositions.remove(descriptor);
}
void blacklist(HintsDescriptor descriptor)

View File

@ -0,0 +1,26 @@
/*
* 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.hints;
/**
* Marker interface for file positions as provided by the various ChecksummedDataReader implementations.
*/
public interface InputPosition
{
long subtract(InputPosition other);
}

View File

@ -152,6 +152,37 @@ public class HintsCatalogTest
assertEquals(0, store.getDispatchQueueSize());
}
@Test
public void deleteHintsTest() throws IOException
{
File directory = Files.createTempDirectory(null).toFile();
UUID hostId1 = UUID.randomUUID();
UUID hostId2 = UUID.randomUUID();
long now = System.currentTimeMillis();
writeDescriptor(directory, new HintsDescriptor(hostId1, now));
writeDescriptor(directory, new HintsDescriptor(hostId1, now+1));
writeDescriptor(directory, new HintsDescriptor(hostId2, now+2));
writeDescriptor(directory, new HintsDescriptor(hostId2, now+3));
// load catalog containing two stores (one for each host)
HintsCatalog catalog = HintsCatalog.load(directory, ImmutableMap.of());
assertEquals(2, catalog.stores().count());
assertTrue(catalog.hasFiles());
// delete all hints from store 1
assertTrue(catalog.get(hostId1).hasFiles());
catalog.deleteAllHints(hostId1);
assertFalse(catalog.get(hostId1).hasFiles());
// stores are still keepts for each host, even after deleting hints
assertEquals(2, catalog.stores().count());
assertTrue(catalog.hasFiles());
// delete all hints from all stores
catalog.deleteAllHints();
assertEquals(2, catalog.stores().count());
assertFalse(catalog.hasFiles());
}
@SuppressWarnings("EmptyTryBlock")
private static void writeDescriptor(File directory, HintsDescriptor descriptor) throws IOException
{

View File

@ -117,9 +117,11 @@ public class HintsCompressionTest
try (HintsReader reader = HintsReader.open(new File(dir, descriptor.fileName())))
{
List<Hint> deserialized = new ArrayList<>(hintNum);
List<InputPosition> pagePositions = new ArrayList<>(hintNum);
for (HintsReader.Page page: reader)
{
pagePositions.add(page.position);
Iterator<Hint> iterator = page.hintsIterator();
while (iterator.hasNext())
{
@ -134,6 +136,21 @@ public class HintsCompressionTest
HintsTestUtil.assertHintsEqual(expected, deserialized.get(hintNum));
hintNum++;
}
// explicitely seek to each page by iterating collected page positions and check if hints still match as expected
int hintOffset = 0;
for (InputPosition pos : pagePositions)
{
reader.seek(pos);
HintsReader.Page page = reader.iterator().next();
Iterator<Hint> iterator = page.hintsIterator();
while (iterator.hasNext())
{
Hint seekedHint = iterator.next();
HintsTestUtil.assertHintsEqual(hints.get(hintOffset), seekedHint);
hintOffset++;
}
}
}
}

View File

@ -0,0 +1,261 @@
/*
* 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.hints;
import java.net.InetAddress;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import com.datastax.driver.core.utils.MoreFutures;
import org.apache.cassandra.SchemaLoader;
import org.apache.cassandra.UpdateBuilder;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.gms.IFailureDetectionEventListener;
import org.apache.cassandra.gms.IFailureDetector;
import org.apache.cassandra.metrics.StorageMetrics;
import org.apache.cassandra.net.MessageIn;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.MockMessagingService;
import org.apache.cassandra.net.MockMessagingSpy;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.FBUtilities;
import static org.apache.cassandra.Util.dk;
import static org.apache.cassandra.net.MockMessagingService.verb;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class HintsServiceTest
{
private static final String KEYSPACE = "hints_service_test";
private static final String TABLE = "table";
private final MockFailureDetector failureDetector = new MockFailureDetector();
@BeforeClass
public static void defineSchema()
{
SchemaLoader.prepareServer();
StorageService.instance.initServer();
SchemaLoader.createKeyspace(KEYSPACE,
KeyspaceParams.simple(1),
SchemaLoader.standardCFMD(KEYSPACE, TABLE));
}
@After
public void cleanup()
{
MockMessagingService.cleanup();
}
@Before
public void reinstanciateService() throws ExecutionException, InterruptedException
{
MessagingService.instance().clearMessageSinks();
if (!HintsService.instance.isShutDown())
{
HintsService.instance.shutdownBlocking();
HintsService.instance.deleteAllHints();
}
failureDetector.isAlive = true;
HintsService.instance = new HintsService(failureDetector);
HintsService.instance.startDispatch();
}
@Test
public void testDispatchHints() throws InterruptedException, ExecutionException
{
long cnt = StorageMetrics.totalHints.getCount();
// create spy for hint messages
MockMessagingSpy spy = sendHintsAndResponses(100, -1);
// metrics should have been updated with number of create hints
assertEquals(cnt + 100, StorageMetrics.totalHints.getCount());
// wait until hints have been send
spy.interceptMessageOut(100).get();
spy.interceptNoMsg(500, TimeUnit.MILLISECONDS).get();
}
@Test
public void testPauseAndResume() throws InterruptedException, ExecutionException
{
HintsService.instance.pauseDispatch();
// create spy for hint messages
MockMessagingSpy spy = sendHintsAndResponses(100, -1);
// we should not send any hints while paused
ListenableFuture<Boolean> noMessagesWhilePaused = spy.interceptNoMsg(15, TimeUnit.SECONDS);
Futures.addCallback(noMessagesWhilePaused, new MoreFutures.SuccessCallback<Boolean>()
{
public void onSuccess(@Nullable Boolean aBoolean)
{
HintsService.instance.resumeDispatch();
}
});
Futures.allAsList(
noMessagesWhilePaused,
spy.interceptMessageOut(100),
spy.interceptNoMsg(200, TimeUnit.MILLISECONDS)
).get();
}
@Test
public void testPageRetry() throws InterruptedException, ExecutionException, TimeoutException
{
// create spy for hint messages, but only create responses for 5 hints
MockMessagingSpy spy = sendHintsAndResponses(20, 5);
Futures.allAsList(
// the dispatcher will always send all hints within the current page
// and only wait for the acks before going to the next page
spy.interceptMessageOut(20),
spy.interceptNoMsg(200, TimeUnit.MILLISECONDS),
// next tick will trigger a retry of the same page as we only replied with 5/20 acks
spy.interceptMessageOut(20)
).get();
// marking the destination node as dead should stop sending hints
failureDetector.isAlive = false;
spy.interceptNoMsg(20, TimeUnit.SECONDS).get();
}
@Test
public void testPageSeek() throws InterruptedException, ExecutionException
{
// create spy for hint messages, stop replying after 12k (should be on 3rd page)
MockMessagingSpy spy = sendHintsAndResponses(20000, 12000);
// At this point the dispatcher will constantly retry the page we stopped acking,
// thus we receive the same hints from the page multiple times and in total more than
// all written hints. Lets just consume them for a while and then pause the dispatcher.
spy.interceptMessageOut(22000).get();
HintsService.instance.pauseDispatch();
Thread.sleep(1000);
// verify that we have a dispatch offset set for the page we're currently stuck at
HintsStore store = HintsService.instance.getCatalog().get(StorageService.instance.getLocalHostUUID());
HintsDescriptor descriptor = store.poll();
store.offerFirst(descriptor); // add again for cleanup during re-instanciation
InputPosition dispatchOffset = store.getDispatchOffset(descriptor);
assertTrue(dispatchOffset != null);
assertTrue(((ChecksummedDataInput.Position) dispatchOffset).sourcePosition > 0);
}
private MockMessagingSpy sendHintsAndResponses(int noOfHints, int noOfResponses)
{
// create spy for hint messages, but only create responses for noOfResponses hints
MessageIn<HintResponse> messageIn = MessageIn.create(FBUtilities.getBroadcastAddress(),
HintResponse.instance,
Collections.emptyMap(),
MessagingService.Verb.REQUEST_RESPONSE,
MessagingService.current_version);
MockMessagingSpy spy;
if (noOfResponses != -1)
{
spy = MockMessagingService.when(verb(MessagingService.Verb.HINT)).respondN(messageIn, noOfResponses);
}
else
{
spy = MockMessagingService.when(verb(MessagingService.Verb.HINT)).respond(messageIn);
}
// create and write noOfHints using service
UUID hostId = StorageService.instance.getLocalHostUUID();
for (int i = 0; i < noOfHints; i++)
{
long now = System.currentTimeMillis();
DecoratedKey dkey = dk(String.valueOf(i));
CFMetaData cfMetaData = Schema.instance.getCFMetaData(KEYSPACE, TABLE);
UpdateBuilder builder = UpdateBuilder.create(cfMetaData, dkey)
.withTimestamp(now)
.newRow("column0")
.add("val", "value0");
Hint hint = Hint.create((Mutation) builder.makeMutation(), now);
HintsService.instance.write(hostId, hint);
}
return spy;
}
private static class MockFailureDetector implements IFailureDetector
{
private boolean isAlive = true;
public boolean isAlive(InetAddress ep)
{
return isAlive;
}
public void interpret(InetAddress ep)
{
throw new UnsupportedOperationException();
}
public void report(InetAddress ep)
{
throw new UnsupportedOperationException();
}
public void registerFailureDetectionEventListener(IFailureDetectionEventListener listener)
{
throw new UnsupportedOperationException();
}
public void unregisterFailureDetectionEventListener(IFailureDetectionEventListener listener)
{
throw new UnsupportedOperationException();
}
public void remove(InetAddress ep)
{
throw new UnsupportedOperationException();
}
public void forceConviction(InetAddress ep)
{
throw new UnsupportedOperationException();
}
}
}