This is an automated email from the ASF dual-hosted git repository.
davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 7b67bc8134 [Fix][Core] Fix enumerator re-registration synchronization
(#10830)
7b67bc8134 is described below
commit 7b67bc81341583f33c379c56587e92dbd5a15087
Author: Jast <[email protected]>
AuthorDate: Wed May 13 20:53:16 2026 +0800
[Fix][Core] Fix enumerator re-registration synchronization (#10830)
---
.../jdbc/source/JdbcSourceSplitEnumeratorTest.java | 84 +++++++
.../server/task/SourceSplitEnumeratorTask.java | 16 +-
.../server/task/SourceSplitEnumeratorTaskTest.java | 253 +++++++++++++++++++++
.../flink/source/FlinkSourceEnumerator.java | 36 ++-
.../flink/source/FlinkSourceEnumeratorTest.java | 91 ++++++++
5 files changed, 462 insertions(+), 18 deletions(-)
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/JdbcSourceSplitEnumeratorTest.java
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/JdbcSourceSplitEnumeratorTest.java
index 4e90b41e70..9f0748d320 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/JdbcSourceSplitEnumeratorTest.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/JdbcSourceSplitEnumeratorTest.java
@@ -203,6 +203,86 @@ class JdbcSourceSplitEnumeratorTest {
Assertions.assertEquals(0, enumerator.currentUnassignedSplitSize());
}
+ @Test
+ void testRegisterReaderReassignsReturnedSplitsWithoutResignaling() throws
Exception {
+ int parallelism = 1;
+ TablePath tablePath = TablePath.of("db", "schema", "table");
+
+ Map<TablePath, JdbcSourceTable> tables = new HashMap<>();
+ tables.put(tablePath, createJdbcSourceTable(tablePath));
+
+ Set<Integer> registeredReaders = new
HashSet<>(Collections.singleton(0));
+ List<List<String>> assignedSplitBatches = new ArrayList<>();
+ AtomicInteger noMoreSplitsCallCount = new AtomicInteger();
+
+ SourceSplitEnumerator.Context<JdbcSourceSplit> context =
+ new SourceSplitEnumerator.Context<JdbcSourceSplit>() {
+ @Override
+ public int currentParallelism() {
+ return parallelism;
+ }
+
+ @Override
+ public Set<Integer> registeredReaders() {
+ return new HashSet<>(registeredReaders);
+ }
+
+ @Override
+ public void assignSplit(int subtaskId,
List<JdbcSourceSplit> splits) {
+ assignedSplitBatches.add(
+ splits.stream()
+ .map(JdbcSourceSplit::splitId)
+
.collect(java.util.stream.Collectors.toList()));
+ }
+
+ @Override
+ public void signalNoMoreSplits(int subtask) {
+ noMoreSplitsCallCount.incrementAndGet();
+ }
+
+ @Override
+ public void sendEventToSourceReader(int subtaskId,
SourceEvent event) {}
+
+ @Override
+ public MetricsContext getMetricsContext() {
+ return null;
+ }
+
+ @Override
+ public EventListener getEventListener() {
+ return null;
+ }
+ };
+
+ JdbcSourceConfig sourceConfig =
+ JdbcSourceConfig.builder()
+ .jdbcConnectionConfig(
+ JdbcConnectionConfig.builder()
+ .url("jdbc:generic://localhost:0/test")
+ .driverName("org.example.Driver")
+ .build())
+ .build();
+
+ JdbcSourceSplitEnumerator enumerator =
+ new JdbcSourceSplitEnumerator(context, sourceConfig, tables,
null);
+
+ enumerator.open();
+ enumerator.run();
+
+ registeredReaders.clear();
+
+ JdbcSourceSplit returnedSplit = createSplit(tablePath,
"returned-split");
+ enumerator.addSplitsBack(Collections.singletonList(returnedSplit), 0);
+
+ registeredReaders.add(0);
+ enumerator.registerReader(0);
+
+ Assertions.assertEquals(2, assignedSplitBatches.size());
+ Assertions.assertEquals(
+ Collections.singletonList("returned-split"),
assignedSplitBatches.get(1));
+ Assertions.assertEquals(1, noMoreSplitsCallCount.get());
+ }
+
private JdbcSourceTable createJdbcSourceTable(TablePath tablePath) {
TableIdentifier tableId = TableIdentifier.of("default", tablePath);
TableSchema tableSchema =
TableSchema.builder().columns(Collections.emptyList()).build();
@@ -211,4 +291,8 @@ class JdbcSourceSplitEnumeratorTest {
tableId, tableSchema, Collections.emptyMap(),
Collections.emptyList(), "");
return
JdbcSourceTable.builder().tablePath(tablePath).catalogTable(catalogTable).build();
}
+
+ private JdbcSourceSplit createSplit(TablePath tablePath, String splitId) {
+ return new JdbcSourceSplit(tablePath, splitId, "SELECT 1", "id", null,
0, 1);
+ }
}
diff --git
a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/SourceSplitEnumeratorTask.java
b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/SourceSplitEnumeratorTask.java
index b22ca022d6..18e1e8217d 100644
---
a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/SourceSplitEnumeratorTask.java
+++
b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/task/SourceSplitEnumeratorTask.java
@@ -224,15 +224,17 @@ public class SourceSplitEnumeratorTask<SplitT extends
SourceSplit> extends Coord
SourceSplitEnumerator<SplitT, Serializable> enumerator =
getEnumerator();
int readerIndex = readerId.getTaskIndex();
+ boolean needResignalNoMoreSplits;
this.addTaskMemberMapping(readerId, memberAddr);
- synchronized (this) {
+ synchronized (enumeratorContext) {
enumerator.registerReader(readerIndex);
- if (enumeratorContext.hasNoMoreSplitsSignaled(readerIndex)) {
- log.info(
- "Reader [{}] re-registered after failover.
Re-signaling NoMoreSplitsEvent.",
- readerIndex);
- enumeratorContext.signalNoMoreSplits(readerIndex);
- }
+ needResignalNoMoreSplits =
enumeratorContext.hasNoMoreSplitsSignaled(readerIndex);
+ }
+ if (needResignalNoMoreSplits) {
+ log.info(
+ "Reader [{}] re-registered after failover. Re-signaling
NoMoreSplitsEvent.",
+ readerIndex);
+ enumeratorContext.signalNoMoreSplits(readerIndex);
}
int taskSize = taskMemberMapping.size();
if (maxReaderSize == taskSize) {
diff --git
a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/task/SourceSplitEnumeratorTaskTest.java
b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/task/SourceSplitEnumeratorTaskTest.java
index b74c8d1dba..0c5cd6bd83 100644
---
a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/task/SourceSplitEnumeratorTaskTest.java
+++
b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/task/SourceSplitEnumeratorTaskTest.java
@@ -18,6 +18,8 @@
package org.apache.seatunnel.engine.server.task;
import org.apache.seatunnel.api.source.SeaTunnelSource;
+import org.apache.seatunnel.api.source.SourceEvent;
+import org.apache.seatunnel.api.source.SourceSplit;
import org.apache.seatunnel.api.source.SourceSplitEnumerator;
import org.apache.seatunnel.engine.common.utils.concurrent.CompletableFuture;
import org.apache.seatunnel.engine.core.dag.actions.SourceAction;
@@ -26,22 +28,42 @@ import
org.apache.seatunnel.engine.server.execution.TaskExecutionContext;
import org.apache.seatunnel.engine.server.execution.TaskGroupLocation;
import org.apache.seatunnel.engine.server.execution.TaskLocation;
import
org.apache.seatunnel.engine.server.task.context.SeaTunnelSplitEnumeratorContext;
+import
org.apache.seatunnel.engine.server.task.operation.source.AssignSplitOperation;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import com.hazelcast.cluster.Address;
+import com.hazelcast.spi.impl.operationservice.Operation;
import com.hazelcast.spi.impl.operationservice.impl.InvocationFuture;
+import java.io.Serializable;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+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.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public class SourceSplitEnumeratorTaskTest {
+ private static final class DummySplit implements SourceSplit {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public String splitId() {
+ return "dummy";
+ }
+ }
+
@Test
void testOpenShouldBeforeReaderRegister() throws Exception {
@@ -160,4 +182,235 @@ public class SourceSplitEnumeratorTaskTest {
Mockito.verify(context, Mockito.times(2)).sendToMember(Mockito.any(),
Mockito.any());
}
+
+ @Test
+ void testReceivedReaderUsesEnumeratorContextLock() throws Exception {
+ SeaTunnelSource<?, DummySplit, Serializable> source =
Mockito.mock(SeaTunnelSource.class);
+ LockAwareEnumerator enumerator = new LockAwareEnumerator();
+
+ AtomicReference<SeaTunnelSplitEnumeratorContext<DummySplit>>
enumeratorContextRef =
+ new AtomicReference<>();
+ Mockito.when(source.createEnumerator(Mockito.any()))
+ .thenAnswer(
+ invocation -> {
+
enumeratorContextRef.set(invocation.getArgument(0));
+ return enumerator;
+ });
+
+ SourceAction<?, DummySplit, Serializable> action =
+ new SourceAction<>(1, "fake", source, new HashSet<>(),
Collections.emptySet());
+ SourceSplitEnumeratorTask<DummySplit> enumeratorTask =
+ new SourceSplitEnumeratorTask<>(
+ 1, new TaskLocation(new TaskGroupLocation(1, 1, 1), 1,
1), action);
+
+ TaskExecutionContext context =
Mockito.mock(TaskExecutionContext.class);
+ InvocationFuture<Object> future = Mockito.mock(InvocationFuture.class);
+
Mockito.when(context.getOrCreateMetricsContext(Mockito.any())).thenReturn(null);
+ Mockito.when(context.sendToMaster(Mockito.any())).thenReturn(future);
+ Mockito.when(context.sendToMember(Mockito.any(),
Mockito.any())).thenReturn(future);
+ Mockito.when(future.join()).thenReturn(null);
+ TaskExecutionService taskExecutionService =
Mockito.mock(TaskExecutionService.class);
+
Mockito.when(context.getTaskExecutionService()).thenReturn(taskExecutionService);
+
+ enumeratorTask.setTaskExecutionContext(context);
+ enumeratorTask.init();
+ enumeratorTask.restoreState(new ArrayList<>());
+
+ TaskLocation readerLocation = new TaskLocation(new
TaskGroupLocation(1, 1, 1), 1, 1);
+ Address address = Address.createUnresolvedAddress("localhost", 5701);
+
+ SeaTunnelSplitEnumeratorContext<DummySplit> enumeratorContext =
enumeratorContextRef.get();
+ Assertions.assertNotNull(enumeratorContext);
+
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
+ try {
+ synchronized (enumeratorContext) {
+ Future<?> blockedRegistration =
+ executorService.submit(
+ () -> {
+ try {
+
enumeratorTask.receivedReader(readerLocation, address);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ Assertions.assertFalse(
+ enumerator.registerReaderCalled.await(200,
TimeUnit.MILLISECONDS));
+ blockedRegistration.cancel(true);
+ }
+
+ Future<?> registrationAfterUnlock =
+ executorService.submit(
+ () -> {
+ try {
+
enumeratorTask.receivedReader(readerLocation, address);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ Assertions.assertTrue(enumerator.registerReaderCalled.await(1,
TimeUnit.SECONDS));
+ registrationAfterUnlock.get(1, TimeUnit.SECONDS);
+ } finally {
+ executorService.shutdownNow();
+ }
+ }
+
+ @Test
+ void
testResignalNoMoreSplitsOutsideLockWithPreciseAssignSplitVerification() throws
Exception {
+ SeaTunnelSource<?, DummySplit, Serializable> source =
Mockito.mock(SeaTunnelSource.class);
+ SourceSplitEnumerator<DummySplit, Serializable> enumerator =
+ Mockito.mock(SourceSplitEnumerator.class);
+
+ AtomicReference<SeaTunnelSplitEnumeratorContext<DummySplit>>
enumeratorContextRef =
+ new AtomicReference<>();
+ Mockito.when(source.createEnumerator(Mockito.any()))
+ .thenAnswer(
+ invocation -> {
+
enumeratorContextRef.set(invocation.getArgument(0));
+ return enumerator;
+ });
+
+ CountDownLatch blockingJoinEntered = new CountDownLatch(1);
+ CountDownLatch allowBlockingJoin = new CountDownLatch(1);
+ CountDownLatch secondReaderRegistered = new CountDownLatch(1);
+
+ Mockito.doAnswer(
+ invocation -> {
+ if (invocation.getArgument(0, Integer.class) == 2)
{
+ secondReaderRegistered.countDown();
+ }
+ return null;
+ })
+ .when(enumerator)
+ .registerReader(Mockito.anyInt());
+
+ SourceAction<?, DummySplit, Serializable> action =
+ new SourceAction<>(1, "fake", source, new HashSet<>(),
Collections.emptySet());
+ SourceSplitEnumeratorTask<DummySplit> enumeratorTask =
+ new SourceSplitEnumeratorTask<>(
+ 1, new TaskLocation(new TaskGroupLocation(1, 1, 1), 1,
1), action);
+
+ TaskExecutionContext context =
Mockito.mock(TaskExecutionContext.class);
+ InvocationFuture<Object> future = Mockito.mock(InvocationFuture.class);
+
Mockito.when(context.getOrCreateMetricsContext(Mockito.any())).thenReturn(null);
+ Mockito.when(context.sendToMaster(Mockito.any())).thenReturn(future);
+ Mockito.when(context.sendToMember(Mockito.any(),
Mockito.any())).thenReturn(future);
+ Mockito.when(future.join())
+ .thenAnswer(
+ invocation -> {
+ blockingJoinEntered.countDown();
+ allowBlockingJoin.await(5, TimeUnit.SECONDS);
+ return null;
+ });
+ TaskExecutionService taskExecutionService =
Mockito.mock(TaskExecutionService.class);
+
Mockito.when(context.getTaskExecutionService()).thenReturn(taskExecutionService);
+
+ enumeratorTask.setTaskExecutionContext(context);
+ enumeratorTask.init();
+ enumeratorTask.restoreState(new ArrayList<>());
+
+ SeaTunnelSplitEnumeratorContext<DummySplit> enumeratorContext =
enumeratorContextRef.get();
+ Assertions.assertNotNull(enumeratorContext);
+ getNoMoreSplitsSignaledReaders(enumeratorContext).add(1);
+
+ TaskLocation firstReader = new TaskLocation(new TaskGroupLocation(1,
1, 1), 1, 1);
+ TaskLocation secondReader = new TaskLocation(new TaskGroupLocation(1,
1, 1), 1, 2);
+ Address firstAddress = Address.createUnresolvedAddress("localhost",
5701);
+ Address secondAddress = Address.createUnresolvedAddress("localhost",
5702);
+
+ ExecutorService executorService = Executors.newFixedThreadPool(2);
+ try {
+ Future<?> blockingFuture =
+ executorService.submit(
+ () -> {
+ try {
+ enumeratorTask.receivedReader(firstReader,
firstAddress);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ Assertions.assertTrue(blockingJoinEntered.await(1,
TimeUnit.SECONDS));
+
+ Future<?> concurrentRegistration =
+ executorService.submit(
+ () -> {
+ try {
+
enumeratorTask.receivedReader(secondReader, secondAddress);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ Assertions.assertTrue(secondReaderRegistered.await(1,
TimeUnit.SECONDS));
+
+ allowBlockingJoin.countDown();
+ blockingFuture.get(1, TimeUnit.SECONDS);
+ concurrentRegistration.get(1, TimeUnit.SECONDS);
+ } finally {
+ executorService.shutdownNow();
+ }
+
+ ArgumentCaptor<Operation> operationCaptor =
ArgumentCaptor.forClass(Operation.class);
+ Mockito.verify(context).sendToMember(operationCaptor.capture(),
Mockito.eq(firstAddress));
+ Operation operation = operationCaptor.getValue();
+ Assertions.assertInstanceOf(AssignSplitOperation.class, operation);
+ Assertions.assertEquals(firstReader, readField(operation, "taskID"));
+ Assertions.assertTrue(((java.util.List<?>) readField(operation,
"splits")).isEmpty());
+ }
+
+ @SuppressWarnings("unchecked")
+ private Set<Integer> getNoMoreSplitsSignaledReaders(
+ SeaTunnelSplitEnumeratorContext<DummySplit> enumeratorContext)
throws Exception {
+ return (Set<Integer>) readField(enumeratorContext,
"noMoreSplitsSignaledReaders");
+ }
+
+ private static Object readField(Object target, String fieldName) throws
Exception {
+ Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return field.get(target);
+ }
+
+ private static final class LockAwareEnumerator
+ implements SourceSplitEnumerator<DummySplit, Serializable> {
+
+ private final CountDownLatch registerReaderCalled = new
CountDownLatch(1);
+
+ @Override
+ public void open() {}
+
+ @Override
+ public void run() {}
+
+ @Override
+ public void close() {}
+
+ @Override
+ public void addSplitsBack(java.util.List<DummySplit> splits, int
subtaskId) {}
+
+ @Override
+ public int currentUnassignedSplitSize() {
+ return 0;
+ }
+
+ @Override
+ public void handleSplitRequest(int subtaskId) {}
+
+ @Override
+ public void registerReader(int subtaskId) {
+ registerReaderCalled.countDown();
+ }
+
+ @Override
+ public void handleSourceEvent(int subtaskId, SourceEvent sourceEvent)
{}
+
+ @Override
+ public Serializable snapshotState(long checkpointId) {
+ return null;
+ }
+
+ @Override
+ public void notifyCheckpointComplete(long checkpointId) {}
+ }
}
diff --git
a/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/source/FlinkSourceEnumerator.java
b/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/source/FlinkSourceEnumerator.java
index bd3dba9c28..960536dcdb 100644
---
a/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/source/FlinkSourceEnumerator.java
+++
b/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/source/FlinkSourceEnumerator.java
@@ -32,9 +32,9 @@ import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
/**
@@ -58,10 +58,10 @@ public class FlinkSourceEnumerator<SplitT extends
SourceSplit, EnumStateT>
private final Set<Integer> noMoreSplitsSignaledReaders;
private final Object lock = new Object();
+ private final Set<Integer> registeredReaderIds = new HashSet<>();
- private AtomicBoolean isRun = new AtomicBoolean(false);
-
- private volatile int currentRegisterReaders = 0;
+ private volatile boolean isRun = false;
+ private volatile boolean isRunning = false;
public FlinkSourceEnumerator(
SourceSplitEnumerator<SplitT, EnumStateT> enumerator,
@@ -96,23 +96,37 @@ public class FlinkSourceEnumerator<SplitT extends
SourceSplit, EnumStateT>
@Override
public void addReader(int subtaskId) {
+ boolean needResignalNoMoreSplits;
+ boolean shouldRun = false;
synchronized (lock) {
sourceSplitEnumerator.registerReader(subtaskId);
- currentRegisterReaders++;
- if (noMoreSplitsSignaledReaders.contains(subtaskId)) {
- LOGGER.info(
- "Reader [{}] re-registered after failover.
Re-signaling NoMoreSplitsEvent.",
- subtaskId);
- enumeratorContext.signalNoMoreSplits(subtaskId);
+ registeredReaderIds.add(subtaskId);
+ needResignalNoMoreSplits =
noMoreSplitsSignaledReaders.contains(subtaskId);
+ if (!isRun && !isRunning && registeredReaderIds.size() ==
parallelism) {
+ shouldRun = true;
+ isRunning = true;
}
}
- if (currentRegisterReaders == parallelism && !isRun.getAndSet(true)) {
+ if (shouldRun) {
try {
sourceSplitEnumerator.run();
+ synchronized (lock) {
+ isRun = true;
+ isRunning = false;
+ }
} catch (Exception e) {
+ synchronized (lock) {
+ isRunning = false;
+ }
throw new RuntimeException(e);
}
}
+ if (needResignalNoMoreSplits) {
+ LOGGER.info(
+ "Reader [{}] re-registered after failover. Re-signaling
NoMoreSplitsEvent.",
+ subtaskId);
+ enumeratorContext.signalNoMoreSplits(subtaskId);
+ }
}
@Override
diff --git
a/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/test/java/org/apache/seatunnel/translation/flink/source/FlinkSourceEnumeratorTest.java
b/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/test/java/org/apache/seatunnel/translation/flink/source/FlinkSourceEnumeratorTest.java
index 949a66f838..83ca41af73 100644
---
a/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/test/java/org/apache/seatunnel/translation/flink/source/FlinkSourceEnumeratorTest.java
+++
b/seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/test/java/org/apache/seatunnel/translation/flink/source/FlinkSourceEnumeratorTest.java
@@ -22,12 +22,18 @@ import
org.apache.seatunnel.api.source.SourceSplitEnumerator;
import org.apache.flink.api.connector.source.SplitEnumeratorContext;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.io.Serializable;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
class FlinkSourceEnumeratorTest {
@@ -59,4 +65,89 @@ class FlinkSourceEnumeratorTest {
Mockito.verify(enumeratorContext).signalNoMoreSplits(0);
}
+
+ @Test
+ void testDuplicateReaderRegistrationDoesNotStartEnumeratorEarly() throws
Exception {
+ SourceSplitEnumerator<DummySplit, Serializable> sourceSplitEnumerator =
+ Mockito.mock(SourceSplitEnumerator.class);
+ SplitEnumeratorContext<SplitWrapper<DummySplit>> enumeratorContext =
+ Mockito.mock(SplitEnumeratorContext.class);
+ Mockito.when(enumeratorContext.currentParallelism()).thenReturn(2);
+
+ FlinkSourceEnumerator<DummySplit, Serializable> enumerator =
+ new FlinkSourceEnumerator<>(
+ sourceSplitEnumerator, enumeratorContext,
ConcurrentHashMap.newKeySet());
+
+ enumerator.addReader(0);
+ enumerator.addReader(0);
+
+ Mockito.verify(sourceSplitEnumerator, Mockito.never()).run();
+
+ enumerator.addReader(1);
+
+ Mockito.verify(sourceSplitEnumerator).run();
+ }
+
+ @Test
+ void testRunFailureCanRetryOnReaderReregister() throws Exception {
+ SourceSplitEnumerator<DummySplit, Serializable> sourceSplitEnumerator =
+ Mockito.mock(SourceSplitEnumerator.class);
+ SplitEnumeratorContext<SplitWrapper<DummySplit>> enumeratorContext =
+ Mockito.mock(SplitEnumeratorContext.class);
+ Mockito.when(enumeratorContext.currentParallelism()).thenReturn(1);
+ Mockito.doThrow(new RuntimeException("run failed"))
+ .doNothing()
+ .when(sourceSplitEnumerator)
+ .run();
+
+ FlinkSourceEnumerator<DummySplit, Serializable> enumerator =
+ new FlinkSourceEnumerator<>(
+ sourceSplitEnumerator, enumeratorContext,
ConcurrentHashMap.newKeySet());
+
+ Assertions.assertThrows(RuntimeException.class, () ->
enumerator.addReader(0));
+
+ enumerator.addReader(0);
+
+ Mockito.verify(sourceSplitEnumerator, Mockito.times(2)).run();
+ }
+
+ @Test
+ void testSnapshotStateDoesNotWaitForBlockingRun() throws Exception {
+ SourceSplitEnumerator<DummySplit, Serializable> sourceSplitEnumerator =
+ Mockito.mock(SourceSplitEnumerator.class);
+ SplitEnumeratorContext<SplitWrapper<DummySplit>> enumeratorContext =
+ Mockito.mock(SplitEnumeratorContext.class);
+ Mockito.when(enumeratorContext.currentParallelism()).thenReturn(1);
+
Mockito.when(sourceSplitEnumerator.snapshotState(1L)).thenReturn("checkpoint");
+
+ CountDownLatch runEntered = new CountDownLatch(1);
+ CountDownLatch releaseRun = new CountDownLatch(1);
+ Mockito.doAnswer(
+ invocation -> {
+ runEntered.countDown();
+ releaseRun.await(5, TimeUnit.SECONDS);
+ return null;
+ })
+ .when(sourceSplitEnumerator)
+ .run();
+
+ FlinkSourceEnumerator<DummySplit, Serializable> enumerator =
+ new FlinkSourceEnumerator<>(
+ sourceSplitEnumerator, enumeratorContext,
ConcurrentHashMap.newKeySet());
+
+ ExecutorService executorService = Executors.newFixedThreadPool(2);
+ Future<?> addReaderFuture = executorService.submit(() ->
enumerator.addReader(0));
+ try {
+ Assertions.assertTrue(runEntered.await(1, TimeUnit.SECONDS));
+
+ Future<Serializable> snapshotFuture =
+ executorService.submit(() -> enumerator.snapshotState(1L));
+
+ Assertions.assertEquals("checkpoint", snapshotFuture.get(1,
TimeUnit.SECONDS));
+ } finally {
+ releaseRun.countDown();
+ addReaderFuture.get(1, TimeUnit.SECONDS);
+ executorService.shutdownNow();
+ }
+ }
}