Biao Geng created FLINK-41206:
---------------------------------
Summary: PyFlink loses Python worker exit status when the worker
process is killed
Key: FLINK-41206
URL: https://issues.apache.org/jira/browse/FLINK-41206
Project: Flink
Issue Type: Improvement
Components: API / Python
Reporter: Biao Geng
h3. Problem
When a Python worker is killed while the TaskManager remains running, Flink may
report only errors such as:
{code:java}
Failed to close remote bundle
CANCELLED: client cancelled
{code}
These messages do not clearly tell users that the Python worker process has
exited. The default TaskManager logs also do not record its exit code. Users
may therefore investigate a communication problem without realizing that the
Python process has died.
h3. Expected behavior
The default TaskManager log should explicitly record that the Python worker has
exited, including its worker ID, PID and exit code, without requiring DEBUG
logging.
This gives users a clear starting point: identify the worker that exited, then
use its PID and exit code to investigate the cause. For example, exit 137 is
consistent with SIGKILL and can help users correlate the failure with
OS/container OOM records. It does not, by itself, prove an OOM kill.
h3. Reproduction
# Start a local standalone session cluster at INFO level. Run a one-row
scalar-UDF job in process mode; have the UDF write {{os.getpid()}} and worker
ID to a marker file, then wait in a loop.
# Once the UDF is entered, send {{kill -9}} to that actual Python PID, leaving
the launcher/JM/TM alone.
# Inspect the task failure and TaskManager log.
The task reports a gRPC/bundle failure, but no explicit worker-exit record with
the Python PID and exit code. These tests deliberately send SIGKILL; they do
not induce an OOM.
h3. Why the exit information is lost
The TaskManager starts a Bash [launcher
script|https://github.com/apache/flink/blob/0addadb5caeead0aedaa780443cce89ae6ff0487/flink-python/pyflink/bin/pyflink-udf-runner.sh],
which then starts Python. Java directly manages the script's process, so the
exit status it receives is the script's.
The script uses {{tee}} to save Python's output. By default, it returns
{{{}tee{}}}'s exit status, so Python can fail while the script reports success.
I tried changing the script to return Python's status. This corrected startup
failures, but exposed another race during runtime failure: after the Python
worker dies, Flink starts cleanup and can terminate the script before it
forwards Python's status. Java then observes the script's termination code,
143, instead of Python's SIGKILL code, 137.
Printing an exit message from the script also does not reliably make it visible
in default TaskManager logs. The script's output is separate from Python's Beam
logging channel, and Beam discards that process output unless Beam's
ProcessManager DEBUG logging is enabled.
h3. Alternatives tested
||Attempt||Result||
|Preserve {{PIPESTATUS}} and print a shell hint|Startup SIGKILL changed exit 0
to 137; missing {{grpc}} changed 0 to 1. Runtime default logs still lacked the
diagnostic.|
|Enable ProcessManager DEBUG|Exposed the shell's 137 hint in MiniCluster JVM
stderr, but requires DEBUG.|
|Read the boot log once on bundle failure|Added startup identity information,
but missed the 137 hint in both runtime tests.|
|Enrich exceptions using a bounded 500 ms probe|Included 137 in an earlier
MiniCluster prototype; rejected from this change because of added
coordination/waiting.|
|Add an INFO callback, retaining Bash|Standalone: Python PID 67309 was killed;
the callback logged Bash PID 67307 / 143 after cleanup.|
|{{exec}} Python, retaining the INFO callback|Two standalone kills logged
actual Python PID / 137. Cancellation logged 143; 1,000 normal rows completed;
missing {{grpc}} retained its traceback and reported 1.|
h3. Proposed change
Replace Bash with Python while retaining output mirroring and the boot log:
{code:java}
log="$BOOT_LOG_DIR/flink-python-udf-boot.log"
exec > >(tee "${log}") 2>&1
exec "${python}" -m pyflink.fn_execution.beam.beam_boot "$@"
{code}
Add one neutral {{Process.onExit()}} INFO record per worker exit:
{code:java}
Process for worker 1-1 exited: pid=38483, exitCode=137
{code}
This removes the separate shell that can lose Python's status during cleanup.
No application-level waiting, polling, exit-classification flags or new
environment factory is proposed. The prototype uses a Flink-local Beam
ProcessManager copy; the hook's integration location remains a review decision.
h3. Validation and limits
The prototype used a formal Maven-packaged JAR, separate standalone
JM/TM/client JVMs and INFO logging. The same Java JAR logged Bash/143 with the
pipeline launcher and Python/137 with {{{}exec{}}}. Worker PID/parent checks
confirmed the process identity. Callback regressions covered exit 0, exit 23
and SIGKILL; module verification passed 370 unit tests and one integration
test. Launcher/boot checks covered cleanup races, log-write failure and delayed
log writing.
The combined prototype was tested on 2.4-SNAPSHOT. Unmodified master
{{0addadb5caee}} also lacks the default exit diagnostic; integrating the
prototype on that master remains outstanding.
Intentional changes: background {{tee}} makes immediate boot-log snapshots best
effort; its failures no longer determine Python's exit status; cleanup SIGTERM
reaches Python directly; every exit adds INFO. A successful job's worker
cleanup also logged 143, so nonzero exits must not automatically be classified
as crashes.
This change adds a log record; it does not guarantee 137 in the final Java
exception, identify OOM automatically, or fix bundle/data-channel deadlocks.
Windows handling is outside this first step. Logging also cannot be guaranteed
if the TaskManager itself is killed.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)