diff --git a/flink-python/src/main/java/org/apache/flink/python/PythonFunctionRunner.java b/flink-python/src/main/java/org/apache/flink/python/PythonFunctionRunner.java
index c637c0e659c13..708e8c70e8a22 100644
--- a/flink-python/src/main/java/org/apache/flink/python/PythonFunctionRunner.java
+++ b/flink-python/src/main/java/org/apache/flink/python/PythonFunctionRunner.java
@@ -34,6 +34,16 @@ public interface PythonFunctionRunner extends AutoCloseable {
/** Tear-down the Python function runner. */
void close() throws Exception;
+ /**
+ * Cancels the Python function runner.
+ *
+ *
The default implementation keeps compatibility with runners that do not need a separate
+ * cancellation path. Runners with a blocking graceful close should override this method.
+ */
+ default void cancel() throws Exception {
+ close();
+ }
+
/**
* Executes the Python function with the input byte array.
*
diff --git a/flink-python/src/main/java/org/apache/flink/streaming/api/operators/python/process/AbstractExternalPythonFunctionOperator.java b/flink-python/src/main/java/org/apache/flink/streaming/api/operators/python/process/AbstractExternalPythonFunctionOperator.java
index da50b5335f22c..f0367fb6b95cf 100644
--- a/flink-python/src/main/java/org/apache/flink/streaming/api/operators/python/process/AbstractExternalPythonFunctionOperator.java
+++ b/flink-python/src/main/java/org/apache/flink/streaming/api/operators/python/process/AbstractExternalPythonFunctionOperator.java
@@ -27,6 +27,7 @@
import org.apache.flink.streaming.api.operators.python.AbstractPythonFunctionOperator;
import org.apache.flink.streaming.api.runners.python.beam.BeamPythonFunctionRunner;
import org.apache.flink.table.functions.python.PythonEnv;
+import org.apache.flink.util.concurrent.ExecutorThreadFactory;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
@@ -55,19 +56,35 @@ public void open() throws Exception {
super.open();
this.pythonFunctionRunner = createPythonFunctionRunner();
this.pythonFunctionRunner.open(config);
- this.flushThreadPool = Executors.newSingleThreadExecutor();
+ this.flushThreadPool =
+ Executors.newSingleThreadExecutor(
+ new ExecutorThreadFactory("beam-python-bundle-flush"));
}
@Override
public void close() throws Exception {
+ final PythonFunctionRunner functionRunner = pythonFunctionRunner;
+ final ExecutorService currentFlushThreadPool = flushThreadPool;
+ final boolean canceling =
+ getContainingTask().isCanceled() || getContainingTask().isFailing();
try {
- if (pythonFunctionRunner != null) {
- pythonFunctionRunner.close();
+ if (canceling && currentFlushThreadPool != null) {
+ currentFlushThreadPool.shutdownNow();
+ }
+
+ if (functionRunner != null) {
+ if (canceling) {
+ functionRunner.cancel();
+ } else {
+ functionRunner.close();
+ }
pythonFunctionRunner = null;
}
- if (flushThreadPool != null) {
- flushThreadPool.shutdown();
+ if (currentFlushThreadPool != null) {
+ if (!canceling) {
+ currentFlushThreadPool.shutdown();
+ }
flushThreadPool = null;
}
} finally {
@@ -78,24 +95,25 @@ public void close() throws Exception {
@Override
protected void invokeFinishBundle() throws Exception {
if (elementCount > 0) {
+ final PythonFunctionRunner functionRunner = pythonFunctionRunner;
AtomicBoolean flushThreadFinish = new AtomicBoolean(false);
AtomicReference exceptionReference = new AtomicReference<>();
flushThreadPool.submit(
() -> {
try {
- pythonFunctionRunner.flush();
+ functionRunner.flush();
} catch (Throwable e) {
exceptionReference.set(e);
} finally {
flushThreadFinish.set(true);
// interrupt the progress of takeResult to avoid the main thread is
// blocked forever.
- ((BeamPythonFunctionRunner) pythonFunctionRunner).notifyNoMoreResults();
+ ((BeamPythonFunctionRunner) functionRunner).notifyNoMoreResults();
}
});
Tuple3 resultTuple;
while (!flushThreadFinish.get()) {
- resultTuple = pythonFunctionRunner.takeResult();
+ resultTuple = functionRunner.takeResult();
if (resultTuple.f2 != 0) {
emitResult(resultTuple);
emitResults();
diff --git a/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/BeamPythonFunctionRunner.java b/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/BeamPythonFunctionRunner.java
index 7d89c93cc36af..82cf75e60d308 100644
--- a/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/BeamPythonFunctionRunner.java
+++ b/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/BeamPythonFunctionRunner.java
@@ -42,6 +42,7 @@
import org.apache.flink.util.Preconditions;
import org.apache.flink.util.ShutdownHookUtil;
import org.apache.flink.util.TemporaryClassLoaderContext;
+import org.apache.flink.util.concurrent.ExecutorThreadFactory;
import org.apache.flink.util.function.LongFunctionWithException;
import org.apache.beam.model.fnexecution.v1.BeamFnApi;
@@ -55,7 +56,6 @@
import org.apache.beam.runners.fnexecution.control.StageBundleFactory;
import org.apache.beam.runners.fnexecution.control.TimerReceiverFactory;
import org.apache.beam.runners.fnexecution.provisioning.JobInfo;
-import org.apache.beam.runners.fnexecution.state.StateRequestHandler;
import org.apache.beam.sdk.coders.ByteArrayCoder;
import org.apache.beam.sdk.coders.Coder;
import org.apache.beam.sdk.fn.data.FnDataReceiver;
@@ -93,6 +93,10 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
@@ -165,7 +169,7 @@ public abstract class BeamPythonFunctionRunner implements PythonFunctionRunner {
private transient StageBundleFactory stageBundleFactory;
/** Handler for state requests. */
- private transient StateRequestHandler stateRequestHandler;
+ private transient BeamStateRequestHandler stateRequestHandler;
/** Handler for bundle progress messages, both during bundle execution and on its completion. */
private transient BundleProgressHandler progressHandler;
@@ -195,6 +199,32 @@ public abstract class BeamPythonFunctionRunner implements PythonFunctionRunner {
/** The shared resource among Python operators of the same slot. */
private transient OpaqueMemoryResource sharedResources;
+ /** Prevents duplicate teardown from explicit close and the JVM shutdown hook. */
+ private transient boolean closed;
+
+ /** Coordinates duplicate close calls until teardown has finished. */
+ @Nullable private transient CompletableFuture closeCompletion;
+
+ /** Whether cancellation should skip waiting for an in-flight bundle close. */
+ private transient boolean cancelRequested;
+
+ /** Prevents duplicate resource teardown from close and cancellation. */
+ private transient boolean resourcesClosed;
+
+ /** Coordinates duplicate close and cancel calls while the state handler drains requests. */
+ @Nullable private transient CompletableFuture stateHandlerCloseCompletion;
+
+ /** Owns a detached bundle close after cancellation returns to the task cleanup path. */
+ @Nullable private transient ExecutorService cancellationBundleCloseExecutor;
+
+ /**
+ * The bundle close currently owned by a flush thread.
+ *
+ * The operation remains reachable while it is running so a concurrent graceful close can
+ * wait for the same bundle close without attempting a second one.
+ */
+ @Nullable private transient BundleCloseOperation bundleCloseOperation;
+
private transient Thread shutdownHook;
private transient Environment environment;
@@ -328,45 +358,74 @@ public void open(ReadableConfig config) throws Exception {
shutdownHook =
ShutdownHookUtil.addShutdownHook(
- this, BeamPythonFunctionRunner.class.getSimpleName(), LOG);
+ this::cancel, BeamPythonFunctionRunner.class.getSimpleName(), LOG);
unregisteredTimers = Collections.synchronizedList(new LinkedList<>());
}
@Override
public void close() throws Exception {
+ if (!startClose()) {
+ awaitCloseCompletion();
+ return;
+ }
+
try {
- if (jobBundleFactory != null) {
- jobBundleFactory.close();
+ try {
+ // Normal shutdown finishes this runner's bundle before tearing down its factory or
+ // shared lease. Cancellation uses the separate non-blocking path below.
+ flush();
+ } finally {
+ try {
+ closeResources();
+ } finally {
+ try {
+ closeStateRequestHandler();
+ } finally {
+ removeShutdownHook();
+ }
+ }
}
} finally {
- jobBundleFactory = null;
+ completeClose();
}
+ }
+
+ @Override
+ public void cancel() throws Exception {
+ final BundleCloseClaim bundleCloseClaim = requestCancel();
try {
- if (sharedResources != null) {
- sharedResources.close();
- } else {
- // if sharedResources is not null, the close of environmentManager will be managed
- // in sharedResources,
- // otherwise, we need to close the environmentManager explicitly
- environmentManager.close();
+ if (bundleCloseClaim != null && bundleCloseClaim.ownsBundleClose) {
+ closeBundleAfterCancellation(bundleCloseClaim.closeOperation);
}
+ // Release only this runner's resource lease. If it is the final lease, the shared
+ // resource disposer owns factory teardown; otherwise co-located runners keep using the
+ // shared worker.
+ closeResources();
} finally {
- sharedResources = null;
- }
-
- if (shutdownHook != null) {
- ShutdownHookUtil.removeShutdownHook(
- shutdownHook, BeamPythonFunctionRunner.class.getSimpleName(), LOG);
- shutdownHook = null;
+ try {
+ // A bundle close already owned by another thread may still issue callbacks while
+ // it unwinds. Gate those callbacks before state backends are disposed.
+ closeStateRequestHandler();
+ } finally {
+ try {
+ removeShutdownHook();
+ } finally {
+ completeClose();
+ }
+ }
}
}
@Override
public void process(byte[] data) throws Exception {
- checkInvokeStartBundle();
- mainInputReceiver.accept(WindowedValues.valueInGlobalWindow(data));
+ final FnDataReceiver> inputReceiver;
+ synchronized (this) {
+ checkInvokeStartBundle();
+ inputReceiver = mainInputReceiver;
+ }
+ inputReceiver.accept(WindowedValues.valueInGlobalWindow(data));
}
@Override
@@ -381,23 +440,34 @@ public void drainUnregisteredTimers() {
@Override
public void processTimer(byte[] timerData) throws Exception {
- if (timerInputReceiver == null) {
- checkInvokeStartBundle();
- timerInputReceiver =
- Preconditions.checkNotNull(
- Iterables.getOnlyElement(remoteBundle.getTimerReceivers().values()),
- "Failed to retrieve main input receiver.");
+ final FnDataReceiver inputReceiver;
+ synchronized (this) {
+ if (timerInputReceiver == null) {
+ checkInvokeStartBundle();
+ timerInputReceiver =
+ Preconditions.checkNotNull(
+ Iterables.getOnlyElement(remoteBundle.getTimerReceivers().values()),
+ "Failed to retrieve main input receiver.");
+ }
+ inputReceiver = timerInputReceiver;
}
Timer timerValue = Timer.cleared(timerData, "", Collections.emptyList());
- timerInputReceiver.accept(timerValue);
+ inputReceiver.accept(timerValue);
}
/** Checks whether to invoke startBundle. */
private void checkInvokeStartBundle() {
+ if (closed) {
+ throw new IllegalStateException("Beam Python function runner is closed.");
+ }
if (!bundleStarted) {
+ if (bundleCloseOperation != null && !bundleCloseOperation.isDone()) {
+ throw new IllegalStateException("The previous Beam bundle is still closing.");
+ }
startBundle();
bundleStarted = true;
+ bundleCloseOperation = null;
}
}
@@ -442,13 +512,34 @@ public Tuple3 takeResult() throws Exception {
@Override
public void flush() throws Exception {
- if (bundleStarted) {
- try {
- finishBundle();
- } finally {
+ final BundleCloseOperation closeOperation;
+ final boolean ownsBundleClose;
+ synchronized (this) {
+ if (cancelRequested) {
+ return;
+ }
+
+ if (bundleStarted) {
+ closeOperation = new BundleCloseOperation(remoteBundle);
+ bundleCloseOperation = closeOperation;
bundleStarted = false;
+ clearBundleReferences();
+ ownsBundleClose = true;
+ } else {
+ closeOperation = bundleCloseOperation;
+ ownsBundleClose = false;
}
}
+
+ if (closeOperation == null) {
+ return;
+ }
+
+ if (ownsBundleClose) {
+ finishBundle(closeOperation);
+ } else {
+ closeOperation.await();
+ }
}
/** Interrupts the progress of takeResult. */
@@ -456,15 +547,249 @@ public void notifyNoMoreResults() {
resultBuffer.add(Tuple2.of(null, new byte[0]));
}
- private void finishBundle() {
+ private void finishBundle(BundleCloseOperation closeOperation) {
+ RuntimeException failure = null;
try {
- remoteBundle.close();
+ closeOperation.remoteBundle.close();
} catch (Throwable t) {
- throw new RuntimeException("Failed to close remote bundle", t);
+ failure = new RuntimeException("Failed to close remote bundle", t);
+ throw failure;
} finally {
- remoteBundle = null;
- mainInputReceiver = null;
- timerInputReceiver = null;
+ closeOperation.complete(failure);
+ if (isCancelRequested()) {
+ try {
+ closeResources();
+ } catch (Exception e) {
+ LOG.warn("Failed to clean up Python resources after cancellation.", e);
+ }
+ }
+ }
+ }
+
+ private synchronized boolean startClose() {
+ if (closed) {
+ return false;
+ }
+ closed = true;
+ closeCompletion = new CompletableFuture<>();
+ return true;
+ }
+
+ /**
+ * Starts cancellation without waiting for a concurrent bundle close.
+ *
+ * If no flush thread owns the active bundle yet, cancellation detaches it and becomes
+ * responsible for finishing it asynchronously.
+ */
+ @Nullable
+ private synchronized BundleCloseClaim requestCancel() {
+ cancelRequested = true;
+ closed = true;
+ if (closeCompletion == null) {
+ closeCompletion = new CompletableFuture<>();
+ }
+
+ if (bundleStarted) {
+ final BundleCloseOperation closeOperation = new BundleCloseOperation(remoteBundle);
+ bundleCloseOperation = closeOperation;
+ bundleStarted = false;
+ clearBundleReferences();
+ return new BundleCloseClaim(closeOperation, true);
+ }
+ if (bundleCloseOperation != null && !bundleCloseOperation.isDone()) {
+ return new BundleCloseClaim(bundleCloseOperation, false);
+ }
+ clearBundleReferences();
+ return null;
+ }
+
+ private synchronized boolean isCancelRequested() {
+ return cancelRequested;
+ }
+
+ private void awaitCloseCompletion() throws Exception {
+ final CompletableFuture currentCloseCompletion;
+ synchronized (this) {
+ currentCloseCompletion = closeCompletion;
+ }
+ if (currentCloseCompletion != null) {
+ awaitCompletion(currentCloseCompletion);
+ }
+ }
+
+ private void completeClose() {
+ final CompletableFuture currentCloseCompletion;
+ synchronized (this) {
+ currentCloseCompletion = closeCompletion;
+ }
+ if (currentCloseCompletion != null) {
+ currentCloseCompletion.complete(null);
+ }
+ }
+
+ private void closeResources() throws Exception {
+ final JobBundleFactory ownedJobBundleFactory;
+ final OpaqueMemoryResource currentSharedResources;
+ synchronized (this) {
+ if (resourcesClosed) {
+ return;
+ }
+ resourcesClosed = true;
+ ownedJobBundleFactory = jobBundleFactory;
+ currentSharedResources = sharedResources;
+ jobBundleFactory = null;
+ sharedResources = null;
+ }
+
+ if (currentSharedResources != null) {
+ try {
+ currentSharedResources.close();
+ } finally {
+ clearBundleReferences();
+ }
+ } else {
+ try {
+ if (ownedJobBundleFactory != null) {
+ ownedJobBundleFactory.close();
+ }
+ } finally {
+ try {
+ environmentManager.close();
+ } finally {
+ clearBundleReferences();
+ }
+ }
+ }
+ }
+
+ private void closeBundleAfterCancellation(BundleCloseOperation closeOperation) {
+ final ExecutorService executor;
+ synchronized (this) {
+ if (cancellationBundleCloseExecutor == null) {
+ cancellationBundleCloseExecutor =
+ Executors.newSingleThreadExecutor(
+ new ExecutorThreadFactory("beam-python-bundle-cancellation"));
+ }
+ executor = cancellationBundleCloseExecutor;
+ }
+ executor.execute(
+ () -> {
+ try {
+ finishBundle(closeOperation);
+ } catch (Throwable t) {
+ LOG.debug("Beam bundle close failed during cancellation.", t);
+ } finally {
+ executor.shutdown();
+ }
+ });
+ }
+
+ private void closeStateRequestHandler() throws Exception {
+ final BeamStateRequestHandler currentStateRequestHandler;
+ final CompletableFuture closeCompletion;
+ final boolean ownsClose;
+ synchronized (this) {
+ if (stateHandlerCloseCompletion == null) {
+ closeCompletion = new CompletableFuture<>();
+ stateHandlerCloseCompletion = closeCompletion;
+ currentStateRequestHandler = stateRequestHandler;
+ stateRequestHandler = null;
+ ownsClose = true;
+ } else {
+ closeCompletion = stateHandlerCloseCompletion;
+ currentStateRequestHandler = null;
+ ownsClose = false;
+ }
+ }
+
+ if (ownsClose) {
+ RuntimeException failure = null;
+ try {
+ if (currentStateRequestHandler != null) {
+ currentStateRequestHandler.close();
+ }
+ } catch (RuntimeException e) {
+ failure = e;
+ throw e;
+ } finally {
+ if (failure == null) {
+ closeCompletion.complete(null);
+ } else {
+ closeCompletion.completeExceptionally(failure);
+ }
+ }
+ } else {
+ awaitCompletion(closeCompletion);
+ }
+ }
+
+ private synchronized void clearBundleReferences() {
+ remoteBundle = null;
+ mainInputReceiver = null;
+ timerInputReceiver = null;
+ }
+
+ private void removeShutdownHook() {
+ final Thread currentShutdownHook;
+ synchronized (this) {
+ currentShutdownHook = shutdownHook;
+ shutdownHook = null;
+ }
+ if (currentShutdownHook != null) {
+ ShutdownHookUtil.removeShutdownHook(
+ currentShutdownHook, BeamPythonFunctionRunner.class.getSimpleName(), LOG);
+ }
+ }
+
+ private static final class BundleCloseOperation {
+
+ private final RemoteBundle remoteBundle;
+ private final CompletableFuture completion = new CompletableFuture<>();
+
+ private BundleCloseOperation(RemoteBundle remoteBundle) {
+ this.remoteBundle = remoteBundle;
+ }
+
+ private boolean isDone() {
+ return completion.isDone();
+ }
+
+ private void complete(@Nullable RuntimeException failure) {
+ if (failure == null) {
+ completion.complete(null);
+ } else {
+ completion.completeExceptionally(failure);
+ }
+ }
+
+ private void await() throws Exception {
+ awaitCompletion(completion);
+ }
+ }
+
+ private static final class BundleCloseClaim {
+
+ private final BundleCloseOperation closeOperation;
+ private final boolean ownsBundleClose;
+
+ private BundleCloseClaim(BundleCloseOperation closeOperation, boolean ownsBundleClose) {
+ this.closeOperation = closeOperation;
+ this.ownsBundleClose = ownsBundleClose;
+ }
+ }
+
+ private static void awaitCompletion(CompletableFuture completion) throws Exception {
+ try {
+ completion.get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw e;
+ } catch (ExecutionException e) {
+ final Throwable cause = e.getCause();
+ if (cause instanceof Exception) {
+ throw (Exception) cause;
+ }
+ throw new RuntimeException(cause);
}
}
@@ -735,7 +1060,7 @@ private TimerReceiverFactory createTimerReceiverFactory() {
return new TimerReceiverFactory(stageBundleFactory, timerDataConsumer, null);
}
- private static StateRequestHandler getStateRequestHandler(
+ private static BeamStateRequestHandler getStateRequestHandler(
@Nullable KeyedStateBackend> keyedStateBackend,
@Nullable OperatorStateBackend operatorStateBackend,
@Nullable TypeSerializer> keySerializer,
diff --git a/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/PythonSharedResources.java b/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/PythonSharedResources.java
index 293d5a5ad83d8..9eaf2fbb4688b 100644
--- a/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/PythonSharedResources.java
+++ b/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/PythonSharedResources.java
@@ -44,6 +44,8 @@ public final class PythonSharedResources implements AutoCloseable {
/** Keep track of the PythonEnvironmentManagers of the Python operators in one slot. */
private final List environmentManagers;
+ private boolean closed;
+
PythonSharedResources(JobBundleFactory jobBundleFactory, Environment environment) {
this.jobBundleFactory = jobBundleFactory;
this.environment = environment;
@@ -63,10 +65,31 @@ synchronized void addPythonEnvironmentManager(PythonEnvironmentManager environme
}
@Override
- public void close() throws Exception {
- jobBundleFactory.close();
+ public synchronized void close() throws Exception {
+ if (closed) {
+ return;
+ }
+
+ Exception exception = null;
+ try {
+ jobBundleFactory.close();
+ } catch (Exception e) {
+ exception = e;
+ }
for (PythonEnvironmentManager environmentManager : environmentManagers) {
- environmentManager.close();
+ try {
+ environmentManager.close();
+ } catch (Exception e) {
+ if (exception == null) {
+ exception = e;
+ } else {
+ exception.addSuppressed(e);
+ }
+ }
+ }
+ closed = true;
+ if (exception != null) {
+ throw exception;
}
}
}
diff --git a/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamStateRequestHandler.java b/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamStateRequestHandler.java
index fb6765dff202d..34295b5a519be 100644
--- a/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamStateRequestHandler.java
+++ b/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamStateRequestHandler.java
@@ -37,12 +37,14 @@
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* The handler for Beam state requests sent from Python side, which does actual operations on Flink
* state.
*/
-public class BeamStateRequestHandler implements StateRequestHandler {
+public class BeamStateRequestHandler implements StateRequestHandler, AutoCloseable {
private final BeamStateStore keyedStateStore;
@@ -54,6 +56,10 @@ public class BeamStateRequestHandler implements StateRequestHandler {
private final BeamFnApi.ProcessBundleRequest.CacheToken cacheToken;
+ private final ReentrantReadWriteLock lifecycleLock = new ReentrantReadWriteLock(true);
+
+ private boolean closed;
+
public BeamStateRequestHandler(
BeamStateStore keyedStateStore,
BeamStateStore operatorStateStore,
@@ -69,23 +75,50 @@ public BeamStateRequestHandler(
@Override
public CompletionStage handle(BeamFnApi.StateRequest request)
throws Exception {
- BeamFnApi.StateKey.TypeCase typeCase = request.getStateKey().getTypeCase();
- ListState listState;
- MapState mapState;
-
- switch (typeCase) {
- case BAG_USER_STATE:
- listState = keyedStateStore.getListState(request);
- return CompletableFuture.completedFuture(
- bagStateHandler.handle(request, listState));
- case MULTIMAP_SIDE_INPUT:
- mapState = keyedStateStore.getMapState(request);
- return CompletableFuture.completedFuture(mapStateHandler.handle(request, mapState));
- case MULTIMAP_KEYS_SIDE_INPUT:
- mapState = operatorStateStore.getMapState(request);
- return CompletableFuture.completedFuture(mapStateHandler.handle(request, mapState));
- default:
- throw new RuntimeException("Unsupported state type: " + typeCase);
+ final Lock readLock = lifecycleLock.readLock();
+ readLock.lock();
+ try {
+ if (closed) {
+ throw new IllegalStateException("Beam state request handler is closed.");
+ }
+
+ BeamFnApi.StateKey.TypeCase typeCase = request.getStateKey().getTypeCase();
+ ListState listState;
+ MapState mapState;
+
+ switch (typeCase) {
+ case BAG_USER_STATE:
+ listState = keyedStateStore.getListState(request);
+ return CompletableFuture.completedFuture(
+ bagStateHandler.handle(request, listState));
+ case MULTIMAP_SIDE_INPUT:
+ mapState = keyedStateStore.getMapState(request);
+ return CompletableFuture.completedFuture(
+ mapStateHandler.handle(request, mapState));
+ case MULTIMAP_KEYS_SIDE_INPUT:
+ mapState = operatorStateStore.getMapState(request);
+ return CompletableFuture.completedFuture(
+ mapStateHandler.handle(request, mapState));
+ default:
+ throw new RuntimeException("Unsupported state type: " + typeCase);
+ }
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * Stops accepting state requests and waits for requests that are already accessing Flink state
+ * to complete.
+ */
+ @Override
+ public void close() {
+ final Lock writeLock = lifecycleLock.writeLock();
+ writeLock.lock();
+ try {
+ closed = true;
+ } finally {
+ writeLock.unlock();
}
}
diff --git a/flink-python/src/test/java/org/apache/flink/streaming/api/operators/python/process/AbstractExternalPythonFunctionOperatorTest.java b/flink-python/src/test/java/org/apache/flink/streaming/api/operators/python/process/AbstractExternalPythonFunctionOperatorTest.java
new file mode 100644
index 0000000000000..fa0d307a562ab
--- /dev/null
+++ b/flink-python/src/test/java/org/apache/flink/streaming/api/operators/python/process/AbstractExternalPythonFunctionOperatorTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.flink.streaming.api.operators.python.process;
+
+import org.apache.flink.api.java.tuple.Tuple3;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.python.PythonFunctionRunner;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.table.functions.python.PythonEnv;
+
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AbstractExternalPythonFunctionOperatorTest {
+
+ @Test
+ void testCloseCancelsRunnerAfterStoppingFlushExecutor() throws Exception {
+ final StreamTask, ?> containingTask = org.mockito.Mockito.mock(StreamTask.class);
+ org.mockito.Mockito.when(containingTask.isCanceled()).thenReturn(true);
+ final ExecutorService flushThreadPool = Executors.newSingleThreadExecutor();
+ final TestingPythonFunctionRunner functionRunner =
+ new TestingPythonFunctionRunner(flushThreadPool);
+ final TestingExternalPythonFunctionOperator operator =
+ createOperator(containingTask, functionRunner, flushThreadPool);
+
+ try {
+ operator.close();
+
+ assertThat(functionRunner.cancelCalled).isTrue();
+ assertThat(functionRunner.closeCalled).isFalse();
+ assertThat(functionRunner.flushExecutorStoppedBeforeCancel).isTrue();
+ assertThat(flushThreadPool.isShutdown()).isTrue();
+ } finally {
+ flushThreadPool.shutdownNow();
+ }
+ }
+
+ @Test
+ void testCloseClosesRunnerGracefullyForNormalCompletion() throws Exception {
+ final StreamTask, ?> containingTask = org.mockito.Mockito.mock(StreamTask.class);
+ final ExecutorService flushThreadPool = Executors.newSingleThreadExecutor();
+ final TestingPythonFunctionRunner functionRunner =
+ new TestingPythonFunctionRunner(flushThreadPool);
+ final TestingExternalPythonFunctionOperator operator =
+ createOperator(containingTask, functionRunner, flushThreadPool);
+
+ try {
+ operator.close();
+
+ assertThat(functionRunner.closeCalled).isTrue();
+ assertThat(functionRunner.cancelCalled).isFalse();
+ assertThat(flushThreadPool.isShutdown()).isTrue();
+ } finally {
+ flushThreadPool.shutdownNow();
+ }
+ }
+
+ private static TestingExternalPythonFunctionOperator createOperator(
+ StreamTask, ?> containingTask,
+ TestingPythonFunctionRunner functionRunner,
+ ExecutorService flushThreadPool)
+ throws ReflectiveOperationException {
+ final TestingExternalPythonFunctionOperator operator =
+ new TestingExternalPythonFunctionOperator(functionRunner);
+ setField(AbstractStreamOperator.class, operator, "container", containingTask);
+ setField(
+ AbstractExternalPythonFunctionOperator.class,
+ operator,
+ "flushThreadPool",
+ flushThreadPool);
+ return operator;
+ }
+
+ private static void setField(Class> owner, Object target, String fieldName, Object value)
+ throws ReflectiveOperationException {
+ final Field field = owner.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(target, value);
+ }
+
+ private static class TestingExternalPythonFunctionOperator
+ extends AbstractExternalPythonFunctionOperator