This is an automated email from the ASF dual-hosted git repository.
kaxilnaik pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new a036dc17024 Fix scheduler ``serve_logs`` subprocess file descriptor
errors (#55443)
a036dc17024 is described below
commit a036dc17024268b42238741c39f46976e11dcfbb
Author: Kaxil Naik <[email protected]>
AuthorDate: Tue Sep 9 16:15:38 2025 -0600
Fix scheduler ``serve_logs`` subprocess file descriptor errors (#55443)
Remove workers parameter from `uvicorn.run()` in `serve_logs` to fix file
descriptor errors when scheduler starts serve_logs as a subprocess.
The original implementation used `workers=2` (copied from Gunicorn) but
this caused multiprocessing issues in containerized environments.
Also implemented lazy app loading via `get_app()` function for better
initialization order and architectural consistency with main API.
The primary fix addresses: OSError: [Errno 9] Bad file descriptor.
Secondary improvement ensures proper initialization timing.
This regression was introduced in
[#52581](https://github.com/apache/airflow/pull/52581) when serve_logs was
refactored from Flask to FastAPI.
---
Without this fix, we see following error when running `airflow standalone`
fresh in an isolated container:
Error 1:
```
scheduler | Traceback (most recent call last):
scheduler | File "/.venv/bin/airflow", line 10, in <module>
scheduler | sys.exit(main())
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/__main__.py", line 55, in main
scheduler | args.func(args)
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/cli/cli_config.py", line 49, in
command
scheduler | return func(*args, **kwargs)
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/utils/cli.py", line 114, in wrapper
scheduler | return f(*args, **kwargs)
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/utils/providers_configuration_loader.py",
line 54, in wrapped_function
scheduler | return func(*args, **kwargs)
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/cli/commands/scheduler_command.py",
line 52, in scheduler
scheduler | run_command_with_daemon_option(
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/cli/commands/daemon_utils.py",
line 86, in run_command_with_daemon_option
scheduler | callback()
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/cli/commands/scheduler_command.py",
line 55, in <lambda>
scheduler | callback=lambda: _run_scheduler_job(args),
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/cli/commands/scheduler_command.py",
line 42, in _run_scheduler_job
scheduler | with _serve_logs(args.skip_serve_logs),
_serve_health_check(enable_health_check):
scheduler | File "/usr/local/lib/python3.10/contextlib.py", line 135, in
__enter__
scheduler | return next(self.gen)
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/cli/commands/scheduler_command.py",
line 62, in _serve_logs
scheduler | from airflow.utils.serve_logs import serve_logs
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/utils/serve_logs/__init__.py",
line 20, in <module>
scheduler | from airflow.utils.serve_logs.log_server import create_app
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/utils/serve_logs/log_server.py",
line 160, in <module>
scheduler | app = create_app()
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/utils/serve_logs/log_server.py",
line 153, in create_app
scheduler | JWTAuthStaticFiles(directory=log_directory, html=False),
scheduler | File
"/.venv/lib/python3.10/site-packages/airflow/utils/serve_logs/log_server.py",
line 49, in __init__
scheduler | super().__init__(*args, **kwargs)
scheduler | File
"/.venv/lib/python3.10/site-packages/starlette/staticfiles.py", line 56, in
__init__
scheduler | raise RuntimeError(f"Directory '{directory}' does not exist")
scheduler | RuntimeError: Directory '/root/airflow/logs' does not exist
```
Error 2:
This was because we had harcoded 2 workers! but this isn't needed
```
scheduler | Process SpawnProcess-1:53:
scheduler | Traceback (most recent call last):
scheduler | File "/usr/local/lib/python3.10/multiprocessing/process.py",
line 314, in _bootstrap
scheduler | self.run()
scheduler | File "/usr/local/lib/python3.10/multiprocessing/process.py",
line 108, in run
scheduler | self._target(*self._args, **self._kwargs)
scheduler | File
"/.venv/lib/python3.10/site-packages/uvicorn/_subprocess.py", line 73, in
subprocess_started
scheduler | sys.stdin = os.fdopen(stdin_fileno) # pragma: full coverage
scheduler | File "/usr/local/lib/python3.10/os.py", line 1030, in fdopen
scheduler | return io.open(fd, mode, buffering, encoding, *args, **kwargs)
scheduler | OSError: [Errno 9] Bad file descriptor
scheduler | Process SpawnProcess-1:54:
scheduler | Traceback (most recent call last):
scheduler | File "/usr/local/lib/python3.10/multiprocessing/process.py",
line 314, in _bootstrap
scheduler | self.run()
scheduler | File "/usr/local/lib/python3.10/multiprocessing/process.py",
line 108, in run
scheduler | self._target(*self._args, **self._kwargs)
scheduler | File
"/.venv/lib/python3.10/site-packages/uvicorn/_subprocess.py", line 73, in
subprocess_started
scheduler | sys.stdin = os.fdopen(stdin_fileno) # pragma: full coverage
scheduler | File "/usr/local/lib/python3.10/os.py", line 1030, in fdopen
scheduler | return io.open(fd, mode, buffering, encoding, *args, **kwargs)
scheduler | OSError: [Errno 9] Bad file descriptor
triggerer | Process SpawnProcess-1:53:
triggerer | Traceback (most recent call last):
triggerer | File "/usr/local/lib/python3.10/multiprocessing/process.py",
line 314, in _bootstrap
triggerer | self.run()
triggerer | File "/usr/local/lib/python3.10/multiprocessing/process.py",
line 108, in run
triggerer | self._target(*self._args, **self._kwargs)
triggerer | File
"/.venv/lib/python3.10/site-packages/uvicorn/_subprocess.py", line 73, in
subprocess_started
triggerer | sys.stdin = os.fdopen(stdin_fileno) # pragma: full coverage
triggerer | File "/usr/local/lib/python3.10/os.py", line 1030, in fdopen
triggerer | return io.open(fd, mode, buffering, encoding, *args, **kwargs)
triggerer | OSError: [Errno 9] Bad file descriptor
```
---
airflow-core/src/airflow/utils/serve_logs/core.py | 7 ++-----
airflow-core/src/airflow/utils/serve_logs/log_server.py | 10 +++++++++-
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/airflow-core/src/airflow/utils/serve_logs/core.py
b/airflow-core/src/airflow/utils/serve_logs/core.py
index c7d15f0d240..9f7543d5076 100644
--- a/airflow-core/src/airflow/utils/serve_logs/core.py
+++ b/airflow-core/src/airflow/utils/serve_logs/core.py
@@ -54,11 +54,8 @@ def serve_logs(port=None):
logger.info("Starting log server on %s", serve_log_uri)
# Use uvicorn directly for ASGI applications
- uvicorn.run("airflow.utils.serve_logs.log_server:app", host=host,
port=port, workers=2, log_level="info")
- # Note: if we want to use more than 1 workers, we **can't** use the
instance of FastAPI directly
- # This is way we split the instantiation of log server to a separate module
- #
- #
https://github.com/encode/uvicorn/blob/374bb6764e8d7f34abab0746857db5e3d68ecfdd/docs/deployment/index.md?plain=1#L50-L63
+ uvicorn.run("airflow.utils.serve_logs.log_server:get_app", host=host,
port=port, log_level="info")
+ # Log serving is I/O bound and has low concurrency, so single process is
sufficient
if __name__ == "__main__":
diff --git a/airflow-core/src/airflow/utils/serve_logs/log_server.py
b/airflow-core/src/airflow/utils/serve_logs/log_server.py
index fa0338e5c9f..16acd64c538 100644
--- a/airflow-core/src/airflow/utils/serve_logs/log_server.py
+++ b/airflow-core/src/airflow/utils/serve_logs/log_server.py
@@ -157,4 +157,12 @@ def create_app():
return fastapi_app
-app = create_app()
+app = None
+
+
+def get_app():
+ """Get or create the FastAPI app instance."""
+ global app
+ if app is None:
+ app = create_app()
+ return app