This is an automated email from the ASF dual-hosted git repository.
JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new c30554f743c Fix fragment failure propagation race (#18543)
c30554f743c is described below
commit c30554f743cbe801a0c2ed6cf59740cc3c63e4b5
Author: Jackie Tien <[email protected]>
AuthorDate: Sun Aug 30 14:07:32 2026 +0800
Fix fragment failure propagation race (#18543)
---
.../execution/exchange/sink/ShuffleSinkHandle.java | 63 ++++++++--
.../execution/exchange/ShuffleSinkHandleTest.java | 134 ++++++++++++++++++++-
2 files changed, 182 insertions(+), 15 deletions(-)
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/exchange/sink/ShuffleSinkHandle.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/exchange/sink/ShuffleSinkHandle.java
index 78e4928fdc3..e54062158a3 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/exchange/sink/ShuffleSinkHandle.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/exchange/sink/ShuffleSinkHandle.java
@@ -33,7 +33,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -65,8 +66,9 @@ public class ShuffleSinkHandle implements ISinkHandle {
private volatile boolean closed = false;
// close() and abort() invoke channel callbacks, so they cannot be protected
by this handle's
- // lock.
- private final AtomicBoolean terminationClaimed = new AtomicBoolean(false);
+ // lock. An abort caller that loses the claim waits for the owner to finish;
otherwise it may
+ // release fragment memory while the owner is still releasing channel
buffers.
+ private final AtomicReference<TerminationClaim> terminationClaim = new
AtomicReference<>();
private static final DataExchangeCostMetricSet DATA_EXCHANGE_COST_METRIC_SET
=
DataExchangeCostMetricSet.getInstance();
@@ -81,6 +83,11 @@ public class ShuffleSinkHandle implements ISinkHandle {
+ RamUsageEstimator.shallowSizeOfInstance(TFragmentInstanceId.class)
+
RamUsageEstimator.shallowSizeOfInstance(DownStreamChannelIndex.class);
+ private static final class TerminationClaim {
+ private final Thread owner = Thread.currentThread();
+ private final CompletableFuture<Void> completion = new
CompletableFuture<>();
+ }
+
public ShuffleSinkHandle(
TFragmentInstanceId localFragmentInstanceId,
List<ISinkChannel> downStreamChannelList,
@@ -149,11 +156,14 @@ public class ShuffleSinkHandle implements ISinkHandle {
@Override
public void setNoMoreTsBlocks() {
- if (closed || aborted) {
+ if (closed || aborted || terminationClaim.get() != null) {
return;
}
try {
lock.lock();
+ if (closed || aborted || terminationClaim.get() != null) {
+ return;
+ }
for (int i = 0; i < downStreamChannelList.size(); i++) {
if (!hasSetNoMoreTsBlocks[i]) {
downStreamChannelList.get(i).setNoMoreTsBlocks();
@@ -168,13 +178,16 @@ public class ShuffleSinkHandle implements ISinkHandle {
@Override
public void setNoMoreTsBlocksOfOneChannel(int channelIndex) {
- if (closed || aborted) {
+ if (closed || aborted || terminationClaim.get() != null) {
// if this ShuffleSinkHandle has been closed, Driver.close() will
attempt to setNoMoreTsBlocks
// for all the channels
return;
}
try {
lock.lock();
+ if (closed || aborted || terminationClaim.get() != null) {
+ return;
+ }
if (!hasSetNoMoreTsBlocks[channelIndex]) {
downStreamChannelList.get(channelIndex).setNoMoreTsBlocks();
hasSetNoMoreTsBlocks[channelIndex] = true;
@@ -206,7 +219,8 @@ public class ShuffleSinkHandle implements ISinkHandle {
@Override
public boolean abort() {
- if (aborted || closed || !terminationClaimed.compareAndSet(false, true)) {
+ TerminationClaim claim = claimTermination(true);
+ if (claim == null) {
return false;
}
try {
@@ -240,9 +254,7 @@ public class ShuffleSinkHandle implements ISinkHandle {
return false;
}
} finally {
- if (!aborted) {
- terminationClaimed.set(false);
- }
+ releaseTerminationClaim(claim);
}
}
@@ -252,7 +264,8 @@ public class ShuffleSinkHandle implements ISinkHandle {
// Lock ShuffleSinkHandle and wait to lock LocalSinkChannel
@Override
public boolean close() {
- if (closed || aborted || !terminationClaimed.compareAndSet(false, true)) {
+ TerminationClaim claim = claimTermination(false);
+ if (claim == null) {
return false;
}
try {
@@ -286,9 +299,7 @@ public class ShuffleSinkHandle implements ISinkHandle {
return false;
}
} finally {
- if (!closed) {
- terminationClaimed.set(false);
- }
+ releaseTerminationClaim(claim);
}
}
@@ -316,6 +327,32 @@ public class ShuffleSinkHandle implements ISinkHandle {
}
}
+ private TerminationClaim claimTermination(boolean waitForCurrentClaim) {
+ TerminationClaim claim = new TerminationClaim();
+ while (!aborted && !closed) {
+ TerminationClaim currentClaim = terminationClaim.get();
+ if (currentClaim == null) {
+ if (terminationClaim.compareAndSet(null, claim)) {
+ return claim;
+ }
+ } else {
+ // A channel callback can re-enter close() while holding the channel
lock. Therefore close()
+ // must not wait for another termination operation. Abort callers are
not invoked under a
+ // channel lock and need the completion barrier before fragment memory
is deregistered.
+ if (!waitForCurrentClaim || currentClaim.owner ==
Thread.currentThread()) {
+ return null;
+ }
+ currentClaim.completion.join();
+ }
+ }
+ return null;
+ }
+
+ private void releaseTerminationClaim(TerminationClaim claim) {
+ terminationClaim.compareAndSet(claim, null);
+ claim.completion.complete(null);
+ }
+
private void switchChannelIfNecessary() {
shuffleStrategy.shuffle();
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/exchange/ShuffleSinkHandleTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/exchange/ShuffleSinkHandleTest.java
index 01725611b6e..15ef07241d4 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/exchange/ShuffleSinkHandleTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/exchange/ShuffleSinkHandleTest.java
@@ -41,6 +41,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
import static
com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
@@ -68,16 +69,25 @@ public class ShuffleSinkHandleTest {
new DownStreamChannelIndex(0),
ShuffleSinkHandle.ShuffleStrategyEnum.PLAIN,
sinkListener);
- ExecutorService executor = Executors.newSingleThreadExecutor();
+ ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Boolean> closeResult = executor.submit(shuffleSinkHandle::close);
Assert.assertTrue(closeStarted.await(5, TimeUnit.SECONDS));
- Assert.assertFalse(shuffleSinkHandle.abort());
+ CountDownLatch abortCalled = new CountDownLatch(1);
+ Future<Boolean> abortResult =
+ executor.submit(
+ () -> {
+ abortCalled.countDown();
+ return shuffleSinkHandle.abort();
+ });
+ Assert.assertTrue(abortCalled.await(5, TimeUnit.SECONDS));
+ Assert.assertFalse(abortResult.isDone());
allowCloseToFinish.countDown();
Assert.assertTrue(closeResult.get(5, TimeUnit.SECONDS));
+ Assert.assertFalse(abortResult.get(5, TimeUnit.SECONDS));
Assert.assertTrue(shuffleSinkHandle.isClosed());
Assert.assertFalse(shuffleSinkHandle.isAborted());
Mockito.verify(channel).close();
@@ -135,6 +145,126 @@ public class ShuffleSinkHandleTest {
}
}
+ @Test
+ public void testConcurrentAbortWaitsForCompletion() throws Exception {
+ TFragmentInstanceId fragmentInstanceId = new TFragmentInstanceId("q0", 0,
"0");
+ ISinkChannel channel = Mockito.mock(ISinkChannel.class);
+ MPPDataExchangeManager.SinkListener sinkListener =
+ Mockito.mock(MPPDataExchangeManager.SinkListener.class);
+ CountDownLatch abortStarted = new CountDownLatch(1);
+ CountDownLatch allowAbortToFinish = new CountDownLatch(1);
+ Mockito.when(channel.abort())
+ .thenAnswer(
+ invocation -> {
+ abortStarted.countDown();
+ Assert.assertTrue(allowAbortToFinish.await(5, TimeUnit.SECONDS));
+ return true;
+ });
+
+ ShuffleSinkHandle shuffleSinkHandle =
+ new ShuffleSinkHandle(
+ fragmentInstanceId,
+ Collections.singletonList(channel),
+ new DownStreamChannelIndex(0),
+ ShuffleSinkHandle.ShuffleStrategyEnum.PLAIN,
+ sinkListener);
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+
+ try {
+ Future<Boolean> firstAbortResult =
executor.submit(shuffleSinkHandle::abort);
+ Assert.assertTrue(abortStarted.await(5, TimeUnit.SECONDS));
+
+ CountDownLatch secondAbortCalled = new CountDownLatch(1);
+ Future<Boolean> secondAbortResult =
+ executor.submit(
+ () -> {
+ secondAbortCalled.countDown();
+ return shuffleSinkHandle.abort();
+ });
+ Assert.assertTrue(secondAbortCalled.await(5, TimeUnit.SECONDS));
+ Assert.assertFalse(secondAbortResult.isDone());
+ allowAbortToFinish.countDown();
+
+ Assert.assertTrue(firstAbortResult.get(5, TimeUnit.SECONDS));
+ Assert.assertFalse(secondAbortResult.get(5, TimeUnit.SECONDS));
+ Mockito.verify(channel).abort();
+ Mockito.verify(sinkListener).onAborted(shuffleSinkHandle);
+ } finally {
+ allowAbortToFinish.countDown();
+ executor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testAbortPreventsConcurrentSetNoMoreTsBlocks() throws Exception {
+ TFragmentInstanceId fragmentInstanceId = new TFragmentInstanceId("q0", 0,
"0");
+ ISinkChannel channel = Mockito.mock(ISinkChannel.class);
+ MPPDataExchangeManager.SinkListener sinkListener =
+ Mockito.mock(MPPDataExchangeManager.SinkListener.class);
+ CountDownLatch abortStarted = new CountDownLatch(1);
+ CountDownLatch allowAbortToFinish = new CountDownLatch(1);
+ Mockito.when(channel.abort())
+ .thenAnswer(
+ invocation -> {
+ abortStarted.countDown();
+ Assert.assertTrue(allowAbortToFinish.await(5, TimeUnit.SECONDS));
+ return true;
+ });
+
+ ShuffleSinkHandle shuffleSinkHandle =
+ new ShuffleSinkHandle(
+ fragmentInstanceId,
+ Collections.singletonList(channel),
+ new DownStreamChannelIndex(0),
+ ShuffleSinkHandle.ShuffleStrategyEnum.PLAIN,
+ sinkListener);
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ try {
+ Future<Boolean> abortResult = executor.submit(shuffleSinkHandle::abort);
+ Assert.assertTrue(abortStarted.await(5, TimeUnit.SECONDS));
+
+ shuffleSinkHandle.setNoMoreTsBlocks();
+
+ Mockito.verify(channel, Mockito.never()).setNoMoreTsBlocks();
+ Mockito.verify(sinkListener,
Mockito.never()).onEndOfBlocks(shuffleSinkHandle);
+ allowAbortToFinish.countDown();
+ Assert.assertTrue(abortResult.get(5, TimeUnit.SECONDS));
+ } finally {
+ allowAbortToFinish.countDown();
+ executor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testCloseDoesNotWaitOnReentrantCall() {
+ TFragmentInstanceId fragmentInstanceId = new TFragmentInstanceId("q0", 0,
"0");
+ ISinkChannel channel = Mockito.mock(ISinkChannel.class);
+ MPPDataExchangeManager.SinkListener sinkListener =
+ Mockito.mock(MPPDataExchangeManager.SinkListener.class);
+ AtomicReference<ShuffleSinkHandle> shuffleSinkHandleReference = new
AtomicReference<>();
+ Mockito.when(channel.close())
+ .thenAnswer(
+ invocation -> {
+ Assert.assertFalse(shuffleSinkHandleReference.get().close());
+ return true;
+ });
+
+ ShuffleSinkHandle shuffleSinkHandle =
+ new ShuffleSinkHandle(
+ fragmentInstanceId,
+ Collections.singletonList(channel),
+ new DownStreamChannelIndex(0),
+ ShuffleSinkHandle.ShuffleStrategyEnum.PLAIN,
+ sinkListener);
+ shuffleSinkHandleReference.set(shuffleSinkHandle);
+
+ Assert.assertTrue(shuffleSinkHandle.close());
+ Assert.assertTrue(shuffleSinkHandle.isClosed());
+ Mockito.verify(channel).close();
+ Mockito.verify(sinkListener).onFinish(shuffleSinkHandle);
+ }
+
@Test
public void testAbort() {
final String queryId = "q0";