Skip to content

Commit

Permalink
Harden PrunableArrayQueue against Pruner implementations that might t…
Browse files Browse the repository at this point in the history
…hrow exceptions

patch by Alex Petrov; reviewed by Caleb Rackliffe and Jon Meredith for CASSANDRA-16866

Co-authored by Alex Petrov <oleksandr.petrov@gmail.com>
Co-authored by Josh McKenzie <jmckenzie@apache.org>
  • Loading branch information
ifesdjeen authored and jmckenzie-dev committed Aug 27, 2021
1 parent 585bc69 commit 2b6799a
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
4.0.1
* Harden PrunableArrayQueue against Pruner implementations that might throw exceptions (CASSANDRA-16866)
* Move RepairedDataInfo to the execution controller rather than the ReadCommand to avoid unintended sharing (CASSANDRA-16721)
* Bump zstd-jni version to 1.5.0-4 (CASSANDRA-16884)
* Remove assumption that all urgent messages are small (CASSANDRA-16877)
Expand Down
49 changes: 46 additions & 3 deletions src/java/org/apache/cassandra/net/PrunableArrayQueue.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import java.util.function.Predicate;

import org.apache.cassandra.utils.Throwables;

/**
* A growing array-based queue that allows efficient bulk in-place removal.
*
Expand Down Expand Up @@ -103,10 +105,12 @@ boolean isEmpty()
*
* @return count of removed elements.
*/
@SuppressWarnings("ThrowFromFinallyBlock")
int prune(Pruner<E> pruner)
{
E e;
int removed = 0;
Throwable error = null;

try
{
Expand All @@ -120,11 +124,35 @@ int prune(Pruner<E> pruner)
int k = (tail - 1 - i) & mask;
e = buffer[k];

if (pruner.shouldPrune(e))
boolean shouldPrune = false;

// If any error has been thrown from the Pruner callbacks, don't bother asking the
// pruner. Just move any elements that need to be moved, correct the head, and rethrow.
if (error == null)
{
try
{
shouldPrune = pruner.shouldPrune(e);
}
catch (Throwable t)
{
error = t;
}
}

if (shouldPrune)
{
buffer[k] = null;
removed++;
pruner.onPruned(e);

try
{
pruner.onPruned(e);
}
catch (Throwable t)
{
error = t;
}
}
else
{
Expand All @@ -133,13 +161,28 @@ int prune(Pruner<E> pruner)
buffer[(k + removed) & mask] = e;
buffer[k] = null;
}
pruner.onKept(e);

try
{
pruner.onKept(e);
}
catch (Throwable t)
{
if (error == null)
{
error = t;
}
}
}
}
}
finally
{
head = (head + removed) & mask;

// Rethrow any error(s) from the Pruner callbacks, but only after the queue state is valid.
if (error != null)
throw Throwables.unchecked(error);
}

return removed;
Expand Down
73 changes: 73 additions & 0 deletions test/unit/org/apache/cassandra/net/PrunableArrayQueueTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@
*/
package org.apache.cassandra.net;

import java.util.Random;

import org.junit.Test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.cassandra.net.PrunableArrayQueue;

import static org.junit.Assert.*;

public class PrunableArrayQueueTest
{
private static final Logger logger = LoggerFactory.getLogger(PrunableArrayQueueTest.class);

private final PrunableArrayQueue<Integer> queue = new PrunableArrayQueue<>(8);

@Test
Expand Down Expand Up @@ -127,4 +134,70 @@ public void onKept(Integer val)
assertEquals((Integer) i, queue.poll());
assertTrue(queue.isEmpty());
}

@Test
public void testUnreliablePruner()
{
long seed = System.currentTimeMillis();
Random rand = new Random(seed);

logger.info("Testing unreliable pruner with random seed {}...", seed);

int iteratons = 100;
int startingQueueSize = 1024;
double pruneChance = 0.1;
double errorOnKeptChance = 0.00005;
double errorOnPruneChance = 0.00002;

for (int i = 0; i < iteratons; i++)
{
int failureValue = rand.nextInt(startingQueueSize);

PrunableArrayQueue<Integer> testQueue = new PrunableArrayQueue<>(startingQueueSize);

for (int o = 0; o < startingQueueSize; o++)
testQueue.offer(o);

class UnreliablePruner implements PrunableArrayQueue.Pruner<Integer>
{
public boolean shouldPrune(Integer value)
{
if (rand.nextDouble() < errorOnPruneChance)
throw new RuntimeException("Failed on pruning check for value: " + value);

return rand.nextDouble() < pruneChance;
}

public void onPruned(Integer value)
{
if (value == failureValue)
throw new RuntimeException("Failed on pruned value: " + value);
}

public void onKept(Integer value)
{
if (rand.nextDouble() < errorOnKeptChance)
throw new RuntimeException("Failed on retained value: " + value);
}
}

assertEquals(startingQueueSize, testQueue.size());

try
{
testQueue.prune(new UnreliablePruner());
}
catch (RuntimeException e)
{
logger.info("Expected pruning failure with seed {}", seed, e);
}

for (int p = 0, postPruneSize = testQueue.size(); p < postPruneSize; p++)
{
assertNotNull("Queue should contain no null elements after pruning. Seed: " + seed + ". Iteration: " + i, testQueue.poll());
}

assertEquals("Queue size should be zero after draining. Seed: " + seed + ". Iteration: " + i, 0, testQueue.size());
}
}
}

0 comments on commit 2b6799a

Please sign in to comment.