Skip to content

Use os.posix_spawn instead of fork+exec in the task-sdk supervisor - #73503

Merged
kaxil merged 2 commits into
apache:mainfrom
seanmuth:seanmuth/posix-spawn-instead-of-fork-exec
Sep 22, 2026
Merged

kaxil merged 2 commits into
apache:mainfrom
seanmuth:seanmuth/posix-spawn-instead-of-fork-exec

Conversation

@seanmuth

Copy link
Copy Markdown
Contributor

execute_tasks_new_python_interpreter (#72164) and the macOS-forced exec path both still call os.fork() before execv(). CPython's os.fork() runs every os.register_at_fork(after_in_child=...) callback synchronously, inside the fork() call itself, before any Python-level code — including the planned execv() — gets control back. A third-party library's own fork handler that blocks there hangs the child before exec is ever reached, regardless of how soon the caller tries to exec — confirmed live: a deployment's task process hung inside datadog's dogstatsd client, which registers exactly such a handler by default. This isn't just Airflow's own known OpenSSL provider-store case (#71707) — it's any library that registers an at-fork handler that isn't async-signal-safe, and fork+exec can't protect against that structurally, no matter how the call sites are ordered.

os.posix_spawn() doesn't have this gap: CPython's binding never calls PyOS_AfterFork_Child(), and glibc's own posix_spawn (2.24+) uses clone(CLONE_VM|CLONE_VFORK) rather than fork(), so registered os.register_at_fork()/pthread_atfork() handlers are structurally unreachable, not just less likely to hang. It's also not a new cost on top of the existing exec path — benchmarked against a real Airflow import, posix_spawn is measurably not more expensive than the fork+exec it replaces (slightly cheaper, from skipping fork()'s own copy-on-write setup before the exec).

No new config surface: wherever use_exec was already True (the platform gate or execute_tasks_new_python_interpreter), the spawn mechanism underneath is now always posix_spawn. That decision is already made by existing config; this only changes how "give me a fresh interpreter" is implemented once it's been decided.

related: #71707, #72164, #72493


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Sonnet 5

Generated-by: Claude Sonnet 5 following the guidelines

Comment thread task-sdk/src/airflow/sdk/execution_time/supervisor.py
Comment thread task-sdk/tests/task_sdk/execution_time/test_supervisor.py
Comment thread task-sdk/src/airflow/sdk/execution_time/supervisor.py
Comment thread task-sdk/src/airflow/sdk/execution_time/supervisor.py
execute_tasks_new_python_interpreter (apache#72164) and the macOS-forced exec
path both still call os.fork() before execv(), and CPython's os.fork()
runs every os.register_at_fork(after_in_child=...) callback synchronously
inside the fork() call itself, before any Python-level code -- including
the planned execv() -- gets control back. A third-party library's own
fork handler that blocks there (confirmed live: a customer's task process
hung inside datadog's dogstatsd client, which registers such a handler by
default) hangs the child before exec is ever reached, regardless of how
soon the caller tries to exec. This isn't just Airflow's own known
OpenSSL provider-store case (apache#71707) -- it's any library that registers
an at-fork handler that isn't async-signal-safe, which fork+exec cannot
protect against structurally, no matter how the call sites are ordered.

os.posix_spawn() doesn't have this gap: CPython's binding never calls
PyOS_AfterFork_Child(), and glibc's own posix_spawn (2.24+) uses
clone(CLONE_VM|CLONE_VFORK) rather than fork(), so registered
os.register_at_fork()/pthread_atfork() handlers are structurally
unreachable, not just less likely to hang. It's also not a new cost on
top of the existing exec path -- benchmarked against a real Airflow
import, posix_spawn is measurably not more expensive than the fork+exec
it replaces (slightly cheaper, from skipping fork()'s own copy-on-write
setup before the exec).

No new config surface: wherever use_exec was already True (the platform
gate or execute_tasks_new_python_interpreter), the spawn mechanism
underneath is now always posix_spawn. That decision was already made by
existing config; this only changes how "give me a fresh interpreter" is
implemented once it's been decided, matching the existing dup2/env/
process-group semantics via posix_spawn's file_actions/env/setpgroup
parameters instead of imperative code in a forked child.
@kaxil
kaxil force-pushed the seanmuth/posix-spawn-instead-of-fork-exec branch from 7ff323a to 1b987ba Compare September 21, 2026 21:11
Fixes docstrings left describing the old os.fork()+os.execv() mechanism
after it was replaced with os.posix_spawn(): the module docstring's
os.set_inheritable/FD_CLOEXEC description, _child_exec_main's "placed
there via dup2" description, and the dup2-ordering safety invariant
that was dropped along with the old imperative dup2 code.
@kaxil kaxil added this to the Airflow 3.3.3 milestone Sep 22, 2026
@kaxil
kaxil merged commit ddfc357 into apache:main Sep 22, 2026
107 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants