This is an automated email from the ASF dual-hosted git repository.
gianm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new ba84c74727d fix: Race between run and cancellation in
FrameProcessorExecutor. (#19006)
ba84c74727d is described below
commit ba84c74727d0f9301409aad8de3849dfc9a33779
Author: Gian Merlino <[email protected]>
AuthorDate: Wed Sep 2 20:14:34 2026 -0700
fix: Race between run and cancellation in FrameProcessorExecutor. (#19006)
Previously, the ExecutorRunnable owned the processor only while running
runProcessorNow(). This created races with cancellation. For example,
if a processor was canceled and cleaned up while the ExecutorRunnable
was calling "isFinished" or "readabilityFuture" on an input channel, it
could lead to calling those operations on a closed channel.
This patch fixes it by having the ExecutorRunnable own the processor
a bit longer, to cover the time that it may need to call methods on
input and output channels. We now also also take care to not call
methods on channels in the debug method logProcessorStatusString.
This patch also makes BlockingQueueFrameChannel more strict about
closing, which helps ensure the above fix is working:
1) Writable channel now rejects writes when closed, rather than when
the reader has finished reading.
2) Writable channel now rejects calls to all methods other than
isClosed() when closed. Behavior changed in: writabilityFuture(),
fail(), and close().
3) Readable channel now rejects calls to all methods when closed.
Behavior changed in: isFinished(), canRead(), read(),
readabilityFuture(), and close().
---
.../output/ChannelStageOutputReaderTest.java | 22 +-
.../frame/channel/BlockingQueueFrameChannel.java | 87 +++++--
.../druid/frame/channel/WritableFrameChannel.java | 15 +-
.../frame/processor/FrameProcessorExecutor.java | 276 ++++++++++++---------
.../druid/frame/processor/ReturnOrAwait.java | 10 +-
.../processor/FrameProcessorExecutorTest.java | 9 +-
.../processor/test/InfiniteFrameProcessor.java | 8 +-
7 files changed, 260 insertions(+), 167 deletions(-)
diff --git
a/multi-stage-query/src/test/java/org/apache/druid/msq/shuffle/output/ChannelStageOutputReaderTest.java
b/multi-stage-query/src/test/java/org/apache/druid/msq/shuffle/output/ChannelStageOutputReaderTest.java
index d556f185535..a0b0e51f935 100644
---
a/multi-stage-query/src/test/java/org/apache/druid/msq/shuffle/output/ChannelStageOutputReaderTest.java
+++
b/multi-stage-query/src/test/java/org/apache/druid/msq/shuffle/output/ChannelStageOutputReaderTest.java
@@ -24,6 +24,7 @@ import com.google.common.io.ByteStreams;
import com.google.common.math.IntMath;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.druid.common.guava.FutureUtils;
+import org.apache.druid.error.DruidException;
import org.apache.druid.error.ThrowableMatcher;
import org.apache.druid.frame.Frame;
import org.apache.druid.frame.FrameType;
@@ -167,12 +168,12 @@ public class ChannelStageOutputReaderTest extends
InitializedNullHandlingTest
channel.writable().write(frame);
// See that we can't write another frame.
- final IllegalStateException e = Assertions.assertThrows(
- IllegalStateException.class,
+ final DruidException e = Assertions.assertThrows(
+ DruidException.class,
() -> channel.writable().write(frame)
);
- ThrowableMatcher.of(IllegalStateException.class)
+ ThrowableMatcher.of(DruidException.class)
.expectMessage(message -> message.startsWith("Channel has
no capacity"))
.assertThat(e);
@@ -192,12 +193,12 @@ public class ChannelStageOutputReaderTest extends
InitializedNullHandlingTest
channel.writable().write(frame);
// See that we can't write a fourth frame.
- final IllegalStateException e2 = Assertions.assertThrows(
- IllegalStateException.class,
+ final DruidException e2 = Assertions.assertThrows(
+ DruidException.class,
() -> channel.writable().write(frame)
);
- ThrowableMatcher.of(IllegalStateException.class)
+ ThrowableMatcher.of(DruidException.class)
.expectMessage(message -> message.startsWith("Channel has
no capacity"))
.assertThat(e2);
@@ -277,7 +278,7 @@ public class ChannelStageOutputReaderTest extends
InitializedNullHandlingTest
private static final int EXPECTED_NUM_ROWS = 1209;
private final BlockingQueueFrameChannel channel = new
BlockingQueueFrameChannel(MAX_FRAMES);
- private final ChannelStageOutputReader reader = new
ChannelStageOutputReader(channel.readable(), FrameTestUtil.WT_CONTEXT_LEGACY);
+ private ChannelStageOutputReader reader = new
ChannelStageOutputReader(channel.readable(), FrameTestUtil.WT_CONTEXT_LEGACY);
@RegisterExtension
public final TemporaryFolderExtension temporaryFolder =
TemporaryFolderExtension.testCaseScoped();
@@ -307,7 +308,9 @@ public class ChannelStageOutputReaderTest extends
InitializedNullHandlingTest
@AfterEach
public void tearDown()
{
- reader.close();
+ if (reader != null) {
+ reader.close();
+ }
}
@Test
@@ -334,6 +337,9 @@ public class ChannelStageOutputReaderTest extends
InitializedNullHandlingTest
IllegalStateException.class,
reader::readLocally
);
+
+ // Prevent close() in tearDown, which would fail.
+ reader = null;
}
@Test
diff --git
a/processing/src/main/java/org/apache/druid/frame/channel/BlockingQueueFrameChannel.java
b/processing/src/main/java/org/apache/druid/frame/channel/BlockingQueueFrameChannel.java
index e434b62c209..9c7abb164d1 100644
---
a/processing/src/main/java/org/apache/druid/frame/channel/BlockingQueueFrameChannel.java
+++
b/processing/src/main/java/org/apache/druid/frame/channel/BlockingQueueFrameChannel.java
@@ -24,9 +24,9 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import it.unimi.dsi.fastutil.objects.ObjectIntPair;
+import org.apache.druid.error.DruidException;
import org.apache.druid.java.util.common.Either;
import org.apache.druid.java.util.common.IAE;
-import org.apache.druid.java.util.common.ISE;
import org.apache.druid.query.rowsandcols.RowsAndColumns;
import javax.annotation.Nullable;
@@ -52,14 +52,31 @@ public class BlockingQueueFrameChannel
private final Writable writable;
private final Readable readable;
+ /**
+ * Queue of items from the writer. Ends with {@link #END_MARKER} if the
writable channel is closed.
+ * Only ever updated by the writer.
+ */
@GuardedBy("lock")
private final ArrayDeque<Optional<Either<Throwable,
ObjectIntPair<RowsAndColumns>>>> queue;
+ /**
+ * Whether {@link Readable#close()} has been called.
+ */
@GuardedBy("lock")
- private SettableFuture<?> readyForWritingFuture = null;
+ private boolean readerClosed;
+ /**
+ * Future that is set to null by {@link #notifyWriter()} when the reader has
read from {@link #queue}
+ * or been closed.
+ */
@GuardedBy("lock")
- private SettableFuture<?> readyForReadingFuture = null;
+ private SettableFuture<?> readyForWritingFuture;
+
+ /**
+ * Future that is set to null by {@link #notifyReader()} when the writer has
written to {@link #queue}.
+ */
+ @GuardedBy("lock")
+ private SettableFuture<?> readyForReadingFuture;
/**
* Create a channel with a particular buffer size (expressed in number of
frames).
@@ -100,13 +117,6 @@ public class BlockingQueueFrameChannel
return new BlockingQueueFrameChannel(1);
}
- private boolean isFinished()
- {
- synchronized (lock) {
- return END_MARKER.equals(queue.peek());
- }
- }
-
@GuardedBy("lock")
private void notifyWriter()
{
@@ -133,16 +143,14 @@ public class BlockingQueueFrameChannel
public void write(RowsAndColumns rac, int partitionNumber)
{
synchronized (lock) {
- if (isFinished()) {
- throw new ISE("Channel cannot accept new frames");
+ if (isClosed()) {
+ throw DruidException.defensive("Channel cannot accept new frames");
} else if (queue.size() >= maxQueuedFrames) {
// Caller should have checked if this channel was ready for writing.
- throw new ISE("Channel has no capacity");
- } else {
- if (!queue.offer(Optional.of(Either.value(ObjectIntPair.of(rac,
partitionNumber))))) {
- // If this happens, it's a bug in this class's capacity-counting.
- throw new ISE("Channel had capacity, but could not add frame");
- }
+ throw DruidException.defensive("Channel has no capacity");
+ } else if (!queue.offer(Optional.of(Either.value(ObjectIntPair.of(rac,
partitionNumber))))) {
+ // If this happens, it's a bug in this class's capacity-counting.
+ throw DruidException.defensive("Channel had capacity, but could not
add frame");
}
notifyReader();
@@ -153,7 +161,9 @@ public class BlockingQueueFrameChannel
public ListenableFuture<?> writabilityFuture()
{
synchronized (lock) {
- if (queue.size() < maxQueuedFrames) {
+ if (isClosed()) {
+ throw DruidException.defensive("Closed, cannot call
writabilityFuture()");
+ } else if (queue.size() < maxQueuedFrames) {
return Futures.immediateFuture(null);
} else if (readyForWritingFuture != null) {
return readyForWritingFuture;
@@ -167,11 +177,15 @@ public class BlockingQueueFrameChannel
public void fail(@Nullable Throwable cause)
{
synchronized (lock) {
+ if (isClosed()) {
+ throw DruidException.defensive("Closed, cannot call fail()");
+ }
+
queue.clear();
- if (!queue.offer(Optional.of(Either.error(cause != null ? cause : new
ISE("Failed"))))) {
+ if (!queue.offer(Optional.of(Either.error(cause != null ? cause : new
RuntimeException("Failed"))))) {
// If this happens, it's a bug, potentially due to incorrectly using
this class with multiple writers.
- throw new ISE("Could not write error to channel");
+ throw DruidException.defensive("Could not write error to channel");
}
notifyReader();
@@ -183,12 +197,12 @@ public class BlockingQueueFrameChannel
{
synchronized (lock) {
if (isClosed()) {
- throw new ISE("Already closed");
+ throw DruidException.defensive("Closed, cannot call close() again");
}
if (!queue.offer(END_MARKER)) {
// If this happens, it's a bug, potentially due to incorrectly using
this class with multiple writers.
- throw new ISE("Channel had capacity, but could not add end marker");
+ throw DruidException.defensive("Channel had capacity, but could not
add end marker");
}
notifyReader();
@@ -210,13 +224,23 @@ public class BlockingQueueFrameChannel
@Override
public boolean isFinished()
{
- return BlockingQueueFrameChannel.this.isFinished();
+ synchronized (lock) {
+ if (readerClosed) {
+ throw DruidException.defensive("Closed, cannot call isFinished()");
+ }
+
+ return END_MARKER.equals(queue.peek());
+ }
}
@Override
public boolean canRead()
{
synchronized (lock) {
+ if (readerClosed) {
+ throw DruidException.defensive("Closed, cannot call canRead()");
+ }
+
return !queue.isEmpty() && !isFinished();
}
}
@@ -227,6 +251,10 @@ public class BlockingQueueFrameChannel
final Optional<Either<Throwable, ObjectIntPair<RowsAndColumns>>> next;
synchronized (lock) {
+ if (readerClosed) {
+ throw DruidException.defensive("Closed, cannot call read()");
+ }
+
if (isFinished()) {
throw new NoSuchElementException();
}
@@ -247,6 +275,10 @@ public class BlockingQueueFrameChannel
public ListenableFuture<?> readabilityFuture()
{
synchronized (lock) {
+ if (readerClosed) {
+ throw DruidException.defensive("Closed, cannot call
readabilityFuture()");
+ }
+
if (!queue.isEmpty()) {
return Futures.immediateFuture(null);
} else if (readyForReadingFuture != null) {
@@ -261,7 +293,12 @@ public class BlockingQueueFrameChannel
public void close()
{
synchronized (lock) {
- queue.clear();
+ if (readerClosed) {
+ // close() should not be called twice.
+ throw DruidException.defensive("Closed, cannot call close() again");
+ }
+
+ readerClosed = true;
notifyWriter();
}
}
diff --git
a/processing/src/main/java/org/apache/druid/frame/channel/WritableFrameChannel.java
b/processing/src/main/java/org/apache/druid/frame/channel/WritableFrameChannel.java
index 170ce4b6844..155b6cfcd3d 100644
---
a/processing/src/main/java/org/apache/druid/frame/channel/WritableFrameChannel.java
+++
b/processing/src/main/java/org/apache/druid/frame/channel/WritableFrameChannel.java
@@ -92,14 +92,19 @@ public interface WritableFrameChannel extends Closeable
*/
void fail(@Nullable Throwable cause) throws IOException;
+ /**
+ * Returns a future that resolves when {@link #write} is able to receive a
new batch of data without blocking or
+ * throwing an exception. The future never resolves to an exception.
+ */
+ ListenableFuture<?> writabilityFuture();
+
/**
* Finish writing to this channel.
*
* When this method is called without {@link #fail(Throwable)} having
previously been called, the writer is
* understood to have completed successfully.
*
- * After calling this method, no additional calls to {@link #write}, {@link
#fail(Throwable)}, or this method
- * are permitted.
+ * After calling this method, no additional calls to are permitted to any
methods other than {@link #isClosed()}.
*/
@Override
void close() throws IOException;
@@ -108,10 +113,4 @@ public interface WritableFrameChannel extends Closeable
* Whether {@link #close()} has been called on this channel.
*/
boolean isClosed();
-
- /**
- * Returns a future that resolves when {@link #write} is able to receive a
new batch of data without blocking or
- * throwing an exception. The future never resolves to an exception.
- */
- ListenableFuture<?> writabilityFuture();
}
diff --git
a/processing/src/main/java/org/apache/druid/frame/processor/FrameProcessorExecutor.java
b/processing/src/main/java/org/apache/druid/frame/processor/FrameProcessorExecutor.java
index f3d378fe576..530232d4e07 100644
---
a/processing/src/main/java/org/apache/druid/frame/processor/FrameProcessorExecutor.java
+++
b/processing/src/main/java/org/apache/druid/frame/processor/FrameProcessorExecutor.java
@@ -32,25 +32,23 @@ import com.google.common.util.concurrent.SettableFuture;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
+import org.apache.druid.error.DruidException;
import org.apache.druid.frame.channel.ReadableFrameChannel;
import org.apache.druid.frame.channel.WritableFrameChannel;
import org.apache.druid.frame.processor.manager.ProcessorManager;
import org.apache.druid.java.util.common.Either;
-import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.concurrent.Execs;
import org.apache.druid.java.util.common.logger.Logger;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
@@ -127,57 +125,82 @@ public class FrameProcessorExecutor
public void run()
{
try {
- final List<ListenableFuture<?>> allWritabilityFutures =
gatherWritabilityFutures();
- final List<ListenableFuture<?>> writabilityFuturesToWaitFor =
- allWritabilityFutures.stream().filter(f ->
!f.isDone()).collect(Collectors.toList());
-
- logProcessorStatusString(processor, finished, allWritabilityFutures);
-
- if (!writabilityFuturesToWaitFor.isEmpty()) {
-
runProcessorAfterFutureResolves(Futures.allAsList(writabilityFuturesToWaitFor),
false);
+ if (!registerRunningProcessor()) {
+ // Processor was canceled. Just exit; cleanup would have been
handled elsewhere.
return;
}
- final Optional<ReturnOrAwait<T>> maybeResult = runProcessorNow();
+ // Set nonnull if the processor should run again.
+ Runnable nextRun = null;
+ Throwable error = null;
- if (!maybeResult.isPresent()) {
- // Processor exited abnormally. Just exit; cleanup would have been
handled elsewhere.
- return;
- }
+ try {
+ final List<ListenableFuture<?>> writabilityFutures =
gatherWritabilityFutures();
+ final List<ListenableFuture<?>> writabilityFuturesToWaitFor =
+ writabilityFutures.stream().filter(f ->
!f.isDone()).collect(Collectors.toList());
+
+ if (!writabilityFuturesToWaitFor.isEmpty()) {
+ logProcessorStatusString(processor, finished.isDone(), null,
null, writabilityFutures);
+ nextRun = () ->
runProcessorAfterFutureResolves(Futures.allAsList(writabilityFuturesToWaitFor),
false);
+ return;
+ }
- final ReturnOrAwait<T> result = maybeResult.get();
- logProcessorStatusString(processor, finished, null);
+ final ReturnOrAwait<T> result = runProcessorNow();
- if (result.isReturn()) {
- succeed(result.value());
- } else if (result.hasAwaitableFutures()) {
-
runProcessorAfterFutureResolves(Futures.allAsList(result.awaitableFutures()),
true);
- } else {
- assert result.hasAwaitableChannels();
+ if (result.isReturn()) {
+ logProcessorStatusString(processor, finished.isDone(), result,
null, null);
+ succeed(result.value());
+ } else if (result.hasAwaitableFutures()) {
+ logProcessorStatusString(processor, finished.isDone(), result,
result.awaitableFutures(), null);
+ final ListenableFuture<List<Object>> combinedFuture =
Futures.allAsList(result.awaitableFutures());
+ nextRun = () -> runProcessorAfterFutureResolves(combinedFuture,
true);
+ } else {
+ assert result.hasAwaitableChannels();
- // Don't retain a reference to this set: it may be mutated the
next time the processor runs.
- final IntSet await = result.awaitableChannels();
+ // Don't retain a reference to this set: it may be mutated the
next time the processor runs.
+ final IntSet await = result.awaitableChannels();
- if (await.isEmpty()) {
- exec.execute(ExecutorRunnable.this);
- } else if (result.isAwaitAllChannels() || await.size() == 1) {
- final List<ListenableFuture<?>> readabilityFutures = new
ArrayList<>();
+ if (await.isEmpty()) {
+ nextRun = () -> exec.execute(ExecutorRunnable.this);
+ } else if (result.isAwaitAllChannels() || await.size() == 1) {
+ final List<ListenableFuture<?>> readabilityFutures = new
ArrayList<>();
- for (final int channelNumber : await) {
- final ReadableFrameChannel channel =
inputChannels.get(channelNumber);
- if (!channel.isFinished() && !channel.canRead()) {
- readabilityFutures.add(channel.readabilityFuture());
+ for (final int channelNumber : await) {
+ final ReadableFrameChannel channel =
inputChannels.get(channelNumber);
+ if (!channel.isFinished() && !channel.canRead()) {
+ readabilityFutures.add(channel.readabilityFuture());
+ }
}
- }
- if (readabilityFutures.isEmpty()) {
- exec.execute(ExecutorRunnable.this);
+ logProcessorStatusString(processor, finished.isDone(), result,
readabilityFutures, null);
+
+ if (readabilityFutures.isEmpty()) {
+ nextRun = () -> exec.execute(ExecutorRunnable.this);
+ } else {
+ final ListenableFuture<List<Object>> combinedFuture =
Futures.allAsList(readabilityFutures);
+ nextRun = () ->
runProcessorAfterFutureResolves(combinedFuture, false);
+ }
} else {
-
runProcessorAfterFutureResolves(Futures.allAsList(readabilityFutures), false);
+ // Await any.
+ final ListenableFuture<?> combinedFuture =
awaitAnyWidget.awaitAny(await);
+ logProcessorStatusString(processor, finished.isDone(), result,
List.of(combinedFuture), null);
+ nextRun = () ->
runProcessorAfterFutureResolves(combinedFuture, false);
}
- } else {
- // Await any.
- runProcessorAfterFutureResolves(awaitAnyWidget.awaitAny(await),
false);
+ }
+ }
+ catch (Throwable e) {
+ error = e;
+ }
+ finally {
+ final boolean canceled = deregisterRunningProcessor();
+
+ if (canceled) {
+ // Processor was canceled while running. Suppress the error and
don't schedule the next run;
+ // the cancel thread handles cleanup.
+ } else if (error != null) {
+ fail(error);
+ } else if (nextRun != null) {
+ nextRun.run();
}
}
}
@@ -198,11 +221,10 @@ public class FrameProcessorExecutor
}
/**
- * Executes {@link FrameProcessor#runIncrementally} on the
currently-readable inputs, while respecting
- * cancellation. Returns an empty Optional if the processor exited
abnormally (canceled or failed). Returns a
- * present Optional if the processor ran successfully. Throws an
exception if the processor does.
+ * Executes {@link FrameProcessor#runIncrementally} on the
currently-readable inputs.
+ * Throws an exception if the processor does.
*/
- private Optional<ReturnOrAwait<T>> runProcessorNow()
+ private ReturnOrAwait<T> runProcessorNow()
{
final IntSet readableInputs = new IntOpenHashSet(inputChannels.size());
@@ -213,21 +235,7 @@ public class FrameProcessorExecutor
}
}
- if (cancellationId != null) {
- // After this synchronized block, our thread may be interrupted by
cancellations, because "cancel"
- // checks "runningProcessors".
- synchronized (lock) {
- if (cancelableProcessors.containsEntry(cancellationId, processor))
{
- runningProcessors.put(processor, Thread.currentThread());
- } else {
- // Processor has been canceled. We don't need to handle cleanup,
because someone else did it.
- return Optional.empty();
- }
- }
- }
-
final String threadName = Thread.currentThread().getName();
- boolean canceled = false;
Either<Throwable, ReturnOrAwait<T>> retVal;
try {
@@ -247,33 +255,11 @@ public class FrameProcessorExecutor
retVal = Either.error(e);
}
finally {
- if (cancellationId != null) {
- // After this synchronized block, our thread will no longer be
interrupted by cancellations,
- // because "cancel" checks "runningProcessors".
- synchronized (lock) {
- if (Thread.interrupted()) {
- // ignore: interrupt was meant for the processor, but came
after the processor already exited.
- }
-
- runningProcessors.remove(processor);
- lock.notifyAll();
-
- if (!cancelableProcessors.containsEntry(cancellationId,
processor)) {
- // Processor has been canceled by one of the "cancel" methods.
They will handle cleanup.
- canceled = true;
- }
- }
-
- // Restore original thread name.
- Thread.currentThread().setName(threadName);
- }
+ // Restore original thread name.
+ Thread.currentThread().setName(threadName);
}
- if (canceled) {
- return Optional.empty();
- } else {
- return Optional.of(retVal.valueOrThrow());
- }
+ return retVal.valueOrThrow();
}
/**
@@ -281,7 +267,7 @@ public class FrameProcessorExecutor
*
* @param future the future
* @param failOnCancel whether the processor should be {@link
#fail(Throwable)} if the future is itself canceled.
- * This is true for futures provided by {@link
ReturnOrAwait#awaitAllFutures(Collection)},
+ * This is true for futures provided by {@link
ReturnOrAwait#awaitAllFutures(List)},
* because the processor has declared it wants to
wait for them; if they are canceled
* the processor must fail. It is false for other
futures, which the processor was not
* directly waiting for.
@@ -384,13 +370,69 @@ public class FrameProcessorExecutor
processor.cleanup();
}
}
+
+ /**
+ * Registers the current thread as running the processor. Returns false
if the processor has been canceled,
+ * in which case the caller should not run it.
+ */
+ private boolean registerRunningProcessor()
+ {
+ if (cancellationId != null) {
+ // After this synchronized block, our thread may be interrupted by
cancellations, because "cancel"
+ // checks "runningProcessors".
+ synchronized (lock) {
+ if (cancelableProcessors.containsEntry(cancellationId, processor))
{
+ final Thread priorThread =
runningProcessors.putIfAbsent(processor, Thread.currentThread());
+ if (priorThread != null) {
+ throw DruidException.defensive(
+ "Already running processor[%s] on thread[%s], cannot also
run on thread[%s]",
+ processor,
+ priorThread.getName(),
+ Thread.currentThread().getName()
+ );
+ }
+ } else {
+ // Processor has been canceled. We don't need to handle cleanup,
because someone else did it.
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Deregisters the current thread from running the processor, clearing
any pending interrupt.
+ *
+ * @return true if the processor was canceled while it was running
+ */
+ private boolean deregisterRunningProcessor()
+ {
+ if (cancellationId != null) {
+ // After this synchronized block, our thread will no longer be
interrupted by cancellations,
+ // because "cancel" checks "runningProcessors".
+ synchronized (lock) {
+ if (Thread.interrupted()) {
+ // ignore: interrupt was meant for the processor, but came after
the processor already exited.
+ }
+
+ runningProcessors.remove(processor);
+ lock.notifyAll();
+
+ return !cancelableProcessors.containsEntry(cancellationId,
processor);
+ }
+ }
+
+ return false;
+ }
}
final ExecutorRunnable runnable = new ExecutorRunnable();
finished.addListener(
() -> {
- logProcessorStatusString(processor, finished, null);
+ // finished will be done here, so this log call gets us the "done=y"
log.
+ logProcessorStatusString(processor, finished.isDone(), null, null,
null);
// If the future was canceled, and the processor is cancelable, then
cancel the processor too.
if (finished.isCancelled() && cancellationId != null) {
@@ -408,7 +450,7 @@ public class FrameProcessorExecutor
Execs.directExecutor()
);
- logProcessorStatusString(processor, finished, null);
+ logProcessorStatusString(processor, finished.isDone(), null, null, null);
registerCancelableProcessor(processor, cancellationId);
exec.execute(runnable);
return finished;
@@ -633,9 +675,22 @@ public class FrameProcessorExecutor
}
}
+ /**
+ * Logs a debug-level status string for the given processor, including input
channel readability,
+ * output channel writability, and completion state.
+ *
+ * @param processor the processor
+ * @param finished whether the processor's "finished" future has resolved
+ * @param returnOrAwait if the processor has just finished {@link
FrameProcessor#runIncrementally}, what it returned
+ * @param readabilityFutures if the processor has just finished {@link
FrameProcessor#runIncrementally}, futures for
+ * inputs it is waiting for
+ * @param writabilityFutures futures for output availability
+ */
private static <T> void logProcessorStatusString(
final FrameProcessor<T> processor,
- final ListenableFuture<?> finishedFuture,
+ final boolean finished,
+ @Nullable final ReturnOrAwait<?> returnOrAwait,
+ @Nullable final List<ListenableFuture<?>> readabilityFutures,
@Nullable final List<ListenableFuture<?>> writabilityFutures
)
{
@@ -643,37 +698,36 @@ public class FrameProcessorExecutor
final StringBuilder sb = new StringBuilder()
.append("Processor [")
.append(processor)
- .append("]; in=[");
+ .append("]");
- for (ReadableFrameChannel channel : processor.inputChannels()) {
- if (channel.canRead()) {
- sb.append("R"); // R for readable
- } else if (channel.isFinished()) {
- sb.append("D"); // D for done
- } else {
- sb.append("~"); // ~ for waiting
- }
- }
+ if (returnOrAwait != null) {
+ sb.append("; ").append(returnOrAwait);
- sb.append("]");
+ if (readabilityFutures != null) {
+ // Information about inputs. Note, we can't call methods on the
processor's input channels, because
+ // they might have been closed.
+ sb.append("; in=[");
+ for (final ListenableFuture<?> future : readabilityFutures) {
+ sb.append(future.isDone() ? 'R' : 'B'); // R = ready; B = blocked.
+ }
+ sb.append("]");
+ }
- if (writabilityFutures != null) {
- sb.append("; out=[");
+ if (writabilityFutures != null) {
+ // Information about outputs. Note, we can't call methods on the
processor's output channels, because
+ // they might have been closed.
+ sb.append("; out=[");
- for (final ListenableFuture<?> future : writabilityFutures) {
- if (future.isDone()) {
- sb.append("W"); // W for writable
- } else {
- sb.append("~"); // ~ for waiting
+ for (final ListenableFuture<?> future : writabilityFutures) {
+ sb.append(future.isDone() ? 'R' : 'B'); // R = ready; B = blocked.
}
- }
- sb.append("]");
+ sb.append("]");
+ }
}
- sb.append("; cancel=").append(finishedFuture.isCancelled() ? "y" : "n");
- sb.append("; done=").append(finishedFuture.isDone() ? "y" : "n");
- log.debug(StringUtils.encodeForFormat(sb.toString()));
+ sb.append("; done=").append(finished ? "y" : "n");
+ log.debug("%s", sb);
}
}
diff --git
a/processing/src/main/java/org/apache/druid/frame/processor/ReturnOrAwait.java
b/processing/src/main/java/org/apache/druid/frame/processor/ReturnOrAwait.java
index c154b425851..9ae6c96d7dd 100644
---
a/processing/src/main/java/org/apache/druid/frame/processor/ReturnOrAwait.java
+++
b/processing/src/main/java/org/apache/druid/frame/processor/ReturnOrAwait.java
@@ -26,7 +26,7 @@ import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.ISE;
import javax.annotation.Nullable;
-import java.util.Collection;
+import java.util.List;
/**
* Instances of this class are returned by {@link
FrameProcessor#runIncrementally}, and are used by
@@ -52,12 +52,12 @@ public class ReturnOrAwait<T>
private final boolean awaitAllChannels;
@Nullable
- private final Collection<ListenableFuture<?>> awaitFutures;
+ private final List<ListenableFuture<?>> awaitFutures;
private ReturnOrAwait(
@Nullable T retVal,
@Nullable IntSet awaitChannels,
- @Nullable Collection<ListenableFuture<?>> awaitFutures,
+ @Nullable List<ListenableFuture<?>> awaitFutures,
final boolean awaitAllChannels
)
{
@@ -106,7 +106,7 @@ public class ReturnOrAwait<T>
/**
* Wait for all of the provided futures.
*/
- public static <T> ReturnOrAwait<T> awaitAllFutures(final
Collection<ListenableFuture<?>> futures)
+ public static <T> ReturnOrAwait<T> awaitAllFutures(final
List<ListenableFuture<?>> futures)
{
return new ReturnOrAwait<>(null, null, futures, true);
}
@@ -161,7 +161,7 @@ public class ReturnOrAwait<T>
}
- public Collection<ListenableFuture<?>> awaitableFutures()
+ public List<ListenableFuture<?>> awaitableFutures()
{
if (!hasAwaitableFutures()) {
throw new ISE("No futures set");
diff --git
a/processing/src/test/java/org/apache/druid/frame/processor/FrameProcessorExecutorTest.java
b/processing/src/test/java/org/apache/druid/frame/processor/FrameProcessorExecutorTest.java
index d1426a7be40..b728c2b7469 100644
---
a/processing/src/test/java/org/apache/druid/frame/processor/FrameProcessorExecutorTest.java
+++
b/processing/src/test/java/org/apache/druid/frame/processor/FrameProcessorExecutorTest.java
@@ -211,7 +211,7 @@ public class FrameProcessorExecutorTest
}
@Test
- public void test_registerCancelableFuture() throws InterruptedException
+ public void test_registerCancelableFuture()
{
final SettableFuture<Object> future = SettableFuture.create();
final String cancellationId = "xyzzy";
@@ -372,15 +372,12 @@ public class FrameProcessorExecutorTest
// If we see an unresolved future here, it's a bug in exec.cancel.
Assertions.assertTrue(future.isDone());
Assertions.assertTrue(future.isCancelled());
-
- final Exception e = Assertions.assertThrows(Exception.class,
future::get);
- Assertions.assertInstanceOf(CancellationException.class, e);
}
}
// In both cases, check for cleanup.
for (final InfiniteFrameProcessor generator : generators) {
- Assertions.assertTrue(generator.didCleanup());
+ Assertions.assertEquals(1, generator.getCleanupCount(), "exactly one
cleanup call");
}
Assertions.assertTrue(chomper.didCleanup());
@@ -388,7 +385,7 @@ public class FrameProcessorExecutorTest
}
@Test
- public void test_cancel_nonexistentCancellationId() throws
InterruptedException
+ public void test_cancel_nonexistentCancellationId()
{
// Just making sure no error is thrown when we refer to a nonexistent
cancellationId.
exec.cancel("nonexistent");
diff --git
a/processing/src/test/java/org/apache/druid/frame/processor/test/InfiniteFrameProcessor.java
b/processing/src/test/java/org/apache/druid/frame/processor/test/InfiniteFrameProcessor.java
index fbbf45f81d8..9443106c456 100644
---
a/processing/src/test/java/org/apache/druid/frame/processor/test/InfiniteFrameProcessor.java
+++
b/processing/src/test/java/org/apache/druid/frame/processor/test/InfiniteFrameProcessor.java
@@ -45,7 +45,7 @@ public class InfiniteFrameProcessor implements
FrameProcessor<Long>
private final Frame frame;
private final WritableFrameChannel outChannel;
private final AtomicBoolean stop = new AtomicBoolean(false);
- private final AtomicBoolean didCleanup = new AtomicBoolean(false);
+ private final AtomicLong cleanupCount = new AtomicLong();
private final AtomicLong numFrames = new AtomicLong();
public InfiniteFrameProcessor(
@@ -86,7 +86,7 @@ public class InfiniteFrameProcessor implements
FrameProcessor<Long>
public void cleanup() throws IOException
{
FrameProcessors.closeAll(inputChannels(), outputChannels());
- didCleanup.set(true);
+ cleanupCount.incrementAndGet();
}
public long getNumFrames()
@@ -99,8 +99,8 @@ public class InfiniteFrameProcessor implements
FrameProcessor<Long>
stop.set(true);
}
- public boolean didCleanup()
+ public long getCleanupCount()
{
- return didCleanup.get();
+ return cleanupCount.get();
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]