This is an automated email from the ASF dual-hosted git repository.

jason810496 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 3f600661aac Add Celery worker mp_start_method config to curb Python 
3.14 memory (#69015)
3f600661aac is described below

commit 3f600661aac6534a4de2ecf32bc1030358919d64
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Fri Jun 26 22:09:31 2026 +0900

    Add Celery worker mp_start_method config to curb Python 3.14 memory (#69015)
    
    Python 3.14 changed the Unix multiprocessing default from fork to forkserver
    (gh-84559). The Celery worker starts its log server and the 
stale-bundle-cleanup
    loop (and the optional SecretCache manager) as stdlib 
multiprocessing.Process
    children. Under forkserver each re-imports Airflow and spins up extra
    forkserver/resource-tracker processes, so a 3.13 to 3.14 upgrade inflates 
the
    worker's resident memory even though nothing about the workload changed.
    
    A new [celery] mp_start_method (falling back to [core] mp_start_method) 
lets a
    deployment pin fork to restore the pre-3.14 behaviour. It governs only the
    worker's stdlib multiprocessing helpers; Celery's prefork pool is driven by
    billiard, which keeps its own fork default and is unaffected either way.
---
 providers/celery/provider.yaml                     | 29 ++++++++++++++++++++++
 .../airflow/providers/celery/cli/celery_command.py | 20 ++++++++++++++-
 .../airflow/providers/celery/get_provider_info.py  | 14 +++++++++++
 .../tests/unit/celery/cli/test_celery_command.py   | 27 +++++++++++++++++++-
 4 files changed, 88 insertions(+), 2 deletions(-)

diff --git a/providers/celery/provider.yaml b/providers/celery/provider.yaml
index 66e2a053452..352a414e0a5 100644
--- a/providers/celery/provider.yaml
+++ b/providers/celery/provider.yaml
@@ -148,6 +148,35 @@ config:
         type: string
         example: ~
         default: "16"
+      mp_start_method:
+        description: |
+          The ``multiprocessing`` start method the ``airflow celery worker`` 
process uses for the
+          standard-library ``multiprocessing`` helpers it starts: the log 
server (``serve_logs``),
+          the stale-bundle-cleanup process, and the optional ``[secrets] 
use_cache`` manager. Must
+          be one of the values returned by 
``multiprocessing.get_all_start_methods()`` on your
+          platform (typically ``fork``, ``forkserver`` or ``spawn``). When 
unset (the default) it
+          falls back to ``[core] mp_start_method`` and then to the platform 
default.
+
+          Python 3.14 changed the Unix default from ``fork`` to 
``forkserver``. ``forkserver`` and
+          ``spawn`` re-import Airflow in each helper and start extra 
forkserver/resource-tracker
+          processes, which increases the worker's resident memory; set this to 
``fork`` to restore
+          the pre-3.14 behaviour. This setting governs the standard-library 
``multiprocessing``
+          helpers only: Celery's ``prefork`` pool is driven by ``billiard`` (a 
separate fork of
+          ``multiprocessing``) and always uses ``fork``, so it is unaffected 
either way.
+        version_added: ~
+        type: string
+        example: "fork"
+        default: ~
+      mp_forkserver_preload:
+        description: |
+          Comma-separated list of modules the ``forkserver`` process should 
import up front, so the
+          worker's ``multiprocessing`` helpers inherit them copy-on-write 
instead of re-importing
+          them. Only used when the effective ``mp_start_method`` is 
``forkserver``. Falls back to
+          ``[core] mp_forkserver_preload`` when unset.
+        version_added: ~
+        type: string
+        example: "airflow"
+        default: ~
       worker_autoscale:
         description: |
           The maximum and minimum number of pool processes that will be used 
to dynamically resize
diff --git 
a/providers/celery/src/airflow/providers/celery/cli/celery_command.py 
b/providers/celery/src/airflow/providers/celery/cli/celery_command.py
index 0e2b66aabf1..3aebe0c2088 100644
--- a/providers/celery/src/airflow/providers/celery/cli/celery_command.py
+++ b/providers/celery/src/airflow/providers/celery/cli/celery_command.py
@@ -37,7 +37,11 @@ from lockfile.pidlockfile import read_pid_from_pidfile, 
remove_existing_pidfile
 from airflow import settings
 from airflow.cli.simple_table import AirflowConsole
 from airflow.exceptions import AirflowConfigException
-from airflow.providers.celery.version_compat import AIRFLOW_V_3_0_PLUS, 
AIRFLOW_V_3_2_PLUS
+from airflow.providers.celery.version_compat import (
+    AIRFLOW_V_3_0_PLUS,
+    AIRFLOW_V_3_2_PLUS,
+    AIRFLOW_V_3_3_PLUS,
+)
 from airflow.providers.common.compat.sdk import conf
 from airflow.utils import cli as cli_utils
 from airflow.utils.cli import setup_locations
@@ -193,6 +197,20 @@ def logger_setup_handler(logger, **kwargs):
 @_providers_configuration_loaded
 def worker(args):
     """Start Airflow Celery worker."""
+    # Apply the configured multiprocessing start method before the worker 
creates any stdlib
+    # multiprocessing objects -- the serve_logs and stale-bundle-cleanup 
helper Processes started
+    # below, and the optional SecretCache Manager. CPython 3.14 switched the 
Unix default from fork
+    # to forkserver (gh-84559); under forkserver those helpers re-import 
Airflow and spin up extra
+    # forkserver/resource_tracker processes, inflating the worker's resident 
memory. Setting
+    # [celery] mp_start_method = fork (or [core] mp_start_method) restores the 
pre-3.14 behaviour.
+    # This governs stdlib multiprocessing only; Celery's prefork pool is 
driven by billiard, which
+    # keeps its own fork default and is unaffected. Guarded because 
set_component_mp_start_method
+    # only exists on Airflow 3.3+.
+    if AIRFLOW_V_3_3_PLUS:
+        from airflow.utils.process_utils import set_component_mp_start_method
+
+        set_component_mp_start_method("celery")
+
     team_config = None
     if hasattr(args, "team") and args.team:
         # Multi-team is enabled, create team-specific Celery app and use team 
based config
diff --git a/providers/celery/src/airflow/providers/celery/get_provider_info.py 
b/providers/celery/src/airflow/providers/celery/get_provider_info.py
index ce59a55918f..b1a663e3495 100644
--- a/providers/celery/src/airflow/providers/celery/get_provider_info.py
+++ b/providers/celery/src/airflow/providers/celery/get_provider_info.py
@@ -75,6 +75,20 @@ def get_provider_info():
                         "example": None,
                         "default": "16",
                     },
+                    "mp_start_method": {
+                        "description": "The ``multiprocessing`` start method 
the ``airflow celery worker`` process uses for the\nstandard-library 
``multiprocessing`` helpers it starts: the log server (``serve_logs``),\nthe 
stale-bundle-cleanup process, and the optional ``[secrets] use_cache`` manager. 
Must\nbe one of the values returned by 
``multiprocessing.get_all_start_methods()`` on your\nplatform (typically 
``fork``, ``forkserver`` or ``spawn``). When unset (the default) it\nfalls ba 
[...]
+                        "version_added": None,
+                        "type": "string",
+                        "example": "fork",
+                        "default": None,
+                    },
+                    "mp_forkserver_preload": {
+                        "description": "Comma-separated list of modules the 
``forkserver`` process should import up front, so the\nworker's 
``multiprocessing`` helpers inherit them copy-on-write instead of 
re-importing\nthem. Only used when the effective ``mp_start_method`` is 
``forkserver``. Falls back to\n``[core] mp_forkserver_preload`` when unset.\n",
+                        "version_added": None,
+                        "type": "string",
+                        "example": "airflow",
+                        "default": None,
+                    },
                     "worker_autoscale": {
                         "description": "The maximum and minimum number of pool 
processes that will be used to dynamically resize\nthe pool based on 
load.Enable autoscaling by providing max_concurrency,min_concurrency\nwith the 
``airflow celery worker`` command (always keep minimum processes,\nbut grow to 
maximum if necessary).\nPick these numbers based on resources on worker box and 
the nature of the task.\nIf autoscale option is available, worker_concurrency 
will be ignored.\nhttps://do [...]
                         "version_added": None,
diff --git a/providers/celery/tests/unit/celery/cli/test_celery_command.py 
b/providers/celery/tests/unit/celery/cli/test_celery_command.py
index 99a9506f3d6..49495e818a5 100644
--- a/providers/celery/tests/unit/celery/cli/test_celery_command.py
+++ b/providers/celery/tests/unit/celery/cli/test_celery_command.py
@@ -36,7 +36,11 @@ from airflow.providers.celery.cli.celery_command import 
_bundle_cleanup_main, _r
 from airflow.providers.common.compat.sdk import conf
 
 from tests_common.test_utils.config import conf_vars
-from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS, 
AIRFLOW_V_3_2_PLUS
+from tests_common.test_utils.version_compat import (
+    AIRFLOW_V_3_0_PLUS,
+    AIRFLOW_V_3_2_PLUS,
+    AIRFLOW_V_3_3_PLUS,
+)
 
 PY313 = sys.version_info >= (3, 13)
 
@@ -195,6 +199,27 @@ class TestWorkerStart:
             ]
         )
 
+    @pytest.mark.skipif(
+        not AIRFLOW_V_3_3_PLUS, reason="set_component_mp_start_method only 
exists on Airflow 3.3+"
+    )
+    @mock.patch("airflow.utils.process_utils.set_component_mp_start_method")
+    
@mock.patch("airflow.providers.celery.cli.celery_command.kombu.pools.reset")
+    @mock.patch("airflow.providers.celery.cli.celery_command.Celery")
+    @mock.patch("airflow.providers.celery.cli.celery_command.setup_locations")
+    @mock.patch("airflow.providers.celery.cli.celery_command.Process")
+    @mock.patch("airflow.providers.celery.executors.celery_executor.app")
+    def test_worker_applies_celery_mp_start_method(
+        self, mock_celery_app, mock_popen, mock_locations, mock_celery_cls, 
mock_pools_reset, mock_set_mp
+    ):
+        # The worker pins its stdlib multiprocessing start method (serve_logs 
/ bundle-cleanup /
+        # SecretCache Manager) from [celery] mp_start_method before spawning 
any helper process.
+        mock_locations.return_value = ("pid_file", None, None, None)
+        args = self.parser.parse_args(["celery", "worker", "--concurrency", 
"1", "--queues", "queue"])
+
+        celery_command.worker(args)
+
+        mock_set_mp.assert_called_once_with("celery")
+
 
 @pytest.mark.backend("mysql", "postgres")
 @pytest.mark.usefixtures("conf_stale_bundle_cleanup_disabled")

Reply via email to