This document describes the current stable version of Celery (5.7). For development docs, go here.

Source code for celery.concurrency.prefork

"""Prefork execution pool.

Pool implementation using :mod:`multiprocessing`.
"""
import os
import threading
import time

from billiard import forking_enable, get_start_method, set_start_method
from billiard.common import REMAP_SIGTERM, TERM_SIGNAME
from billiard.pool import CLOSE, RUN
from billiard.pool import Pool as BlockingPool
from kombu.asynchronous import get_event_loop
from kombu.message import Message

from celery import platforms, signals
from celery._state import _set_task_join_will_block, set_default_app
from celery.app import base as _app_base
from celery.app import trace
from celery.concurrency.base import BasePool
from celery.utils.functional import noop
from celery.utils.log import get_logger

from .asynpool import AsynPool

__all__ = ('TaskPool', 'process_initializer', 'process_destructor')

#: List of signals to reset when a child process starts.
WORKER_SIGRESET = {
    'SIGTERM', 'SIGHUP', 'SIGTTIN', 'SIGTTOU', 'SIGUSR1',
}

#: List of signals to ignore when a child process starts.
if REMAP_SIGTERM:
    WORKER_SIGIGNORE = {'SIGINT', TERM_SIGNAME}
else:
    WORKER_SIGIGNORE = {'SIGINT'}

logger = get_logger(__name__)
warning, debug = logger.warning, logger.debug

#: Host name of the worker this pool process belongs to, set by
#: :func:`process_initializer` for :func:`process_destructor`.
_process_hostname = None


[docs] def process_initializer(app, hostname): """Pool child process initializer. Initialize the child pool process to ensure the correct app instance is used and things like logging works. """ # Each running worker gets SIGKILL by OS when main process exits. platforms.set_pdeathsig('SIGKILL') _set_task_join_will_block(True) platforms.signals.reset(*WORKER_SIGRESET) platforms.signals.ignore(*WORKER_SIGIGNORE) platforms.set_mp_process_title('celeryd', hostname=hostname) if get_start_method() != 'fork': # billiard started this child as a fresh interpreter on its own (for # example the macOS default since billiard 4.3), so announce it the # same way the parent does for worker_pool_start_method 'spawn'. # Both have to be set before init_worker() imports the task modules: # the environment variable for whatever this process starts later, # and the flag in the already-imported module that decides how # @app.task binds. os.environ['FORKED_BY_MULTIPROCESSING'] = '1' _app_base.USING_EXECV = True # This is for Windows and other platforms not supporting # fork(). Note that init_worker makes sure it's only # run once per process. app.loader.init_worker() app.loader.init_worker_process() logfile = os.environ.get('CELERY_LOG_FILE') or None if logfile and '%i' in logfile.lower(): # logfile path will differ so need to set up logging again. app.log.already_setup = False app.log.setup(int(os.environ.get('CELERY_LOG_LEVEL', 0) or 0), logfile, bool(os.environ.get('CELERY_LOG_REDIRECT', False)), str(os.environ.get('CELERY_LOG_REDIRECT_LEVEL')), hostname=hostname) if os.environ.get('FORKED_BY_MULTIPROCESSING'): # the child is a fresh interpreter (spawn, forkserver or execv) trace.setup_worker_optimizations(app, hostname) else: app.set_current() set_default_app(app) app.finalize() trace._tasks = app._tasks # enables fast_trace_task optimization. # rebuild execution handler for all tasks. from celery.app.trace import build_tracer for name, task in app.tasks.items(): task.__trace__ = build_tracer(name, task, app.loader, hostname, app=app) from celery.worker import state as worker_state worker_state.reset_state() global _process_hostname _process_hostname = hostname signals.worker_process_init.send(sender=hostname)
[docs] def process_destructor(pid, exitcode): """Pool child process destructor. Dispatch the :signal:`worker_process_shutdown` signal. """ signals.worker_process_shutdown.send( sender=_process_hostname, pid=pid, exitcode=exitcode, )
def _is_ack_callback(callback): # The consumer queues ``promise(message.ack_log_error)`` and # ``promise(message.reject_log_error)``. Match the bound method by owner # type and name so a transport's Message subclass overriding them still # counts; anything unrecognised is left for ``hub.close()``. fun = getattr(callback, 'fun', None) return (isinstance(getattr(fun, '__self__', None), Message) and getattr(fun, '__name__', None) in ('ack_log_error', 'reject_log_error')) def _run_ready_callbacks(hub): """Run the task ack/reject callbacks queued with ``hub.call_soon()``. With ``task_acks_late`` the acknowledgement of a task that finished while the pool is being joined is one of these, and nothing else runs them before ``hub.close()``, which waits for the join. Only the ``Message.ack_log_error`` / ``reject_log_error`` promises are run: the queue can also hold the transport's socket reader, which re-queues itself after every read and would hand deliveries to the closed pool, so anything else is put back for ``hub.close()``. Pop one at a time: the main thread may still add to the set we drained (``Consumer.on_send_event_buffered`` bypasses the lock) and must not abort the batch. """ ready = hub._pop_ready() while ready: callback = ready.pop() if not _is_ack_callback(callback): hub.call_soon(callback) continue try: callback() except Exception as exc: # pylint: disable=broad-except hub.on_callback_error(callback, exc)
[docs] class TaskPool(BasePool): """Multiprocessing Pool implementation.""" Pool = AsynPool BlockingPool = BlockingPool uses_semaphore = True write_stats = None
[docs] def on_start(self): if self.forking_enable: forking_enable(True) else: # billiard's forking_enable(False) maps to the legacy execv # mechanism, which depends on the optional _billiard C extension # and is unavailable on CPython 3 (always warns and silently # stays on fork). Use the modern 'spawn' start method instead, # which is implemented in pure Python and actually takes effect. # # Each spawned child is a fresh interpreter and therefore must # re-run the worker optimizations (task registry shortcut used by # fast_trace_task, Django model validation, etc.). The rest of # Celery keys this "fresh interpreter" behavior off the # FORKED_BY_MULTIPROCESSING environment variable (historically set # by billiard's execv path), so set it for the spawned children. os.environ['FORKED_BY_MULTIPROCESSING'] = '1' set_start_method('spawn', force=True) Pool = (self.BlockingPool if self.options.get('threads', True) else self.Pool) proc_alive_timeout = ( self.app.conf.worker_proc_alive_timeout if self.app else None ) P = self._pool = Pool(processes=self.limit, initializer=process_initializer, on_process_exit=process_destructor, enable_timeouts=True, synack=False, proc_alive_timeout=proc_alive_timeout, **self.options) # Create proxy methods self.on_apply = P.apply_async self.maintain_pool = P.maintain_pool self.terminate_job = P.terminate_job self.grow = P.grow self.shrink = P.shrink self.flush = getattr(P, 'flush', None) # FIXME add to billiard
[docs] def restart(self): self._pool.restart() self._pool.apply_async(noop)
[docs] def did_start_ok(self): return self._pool.did_start_ok()
[docs] def register_with_event_loop(self, loop): try: reg = self._pool.register_with_event_loop except AttributeError: return return reg(loop)
[docs] def on_stop(self): """Gracefully stop the pool.""" if self._pool is not None and self._pool._state in (RUN, CLOSE): self._pool.close() # Keep firing timers (for heartbeats on async transports) while # the pool drains. If not using an async transport, no hub exists # and the timer thread is not created. hub = get_event_loop() if hub is not None: shutdown_event = threading.Event() def fire_timers_loop(): while not shutdown_event.is_set(): try: hub.fire_timers() _run_ready_callbacks(hub) except Exception: logger.warning( "Exception in timer thread during prefork on_stop()", exc_info=True, ) # 0.5 seconds was chosen as a balance between joining quickly # after the pool join is complete and sleeping long enough to # avoid excessive CPU usage. time.sleep(0.5) timer_thread = threading.Thread( target=fire_timers_loop, daemon=True, name="prefork-timer-shutdown", ) timer_thread.start() try: self._pool.join() finally: shutdown_event.set() timer_thread.join(timeout=1.0) if timer_thread.is_alive(): logger.warning( "Timer thread in prefork on_stop() did not terminate cleanly" ) else: # The results the join waited for queued their acks # after the thread's last pass. try: _run_ready_callbacks(hub) except Exception: logger.warning( "Exception running ready callbacks after prefork pool join", exc_info=True, ) else: self._pool.join() self._pool = None
[docs] def on_terminate(self): """Force terminate the pool.""" if self._pool is not None: self._pool.terminate() self._pool = None
[docs] def on_close(self): if self._pool is not None and self._pool._state == RUN: self._pool.close()
def _get_info(self): write_stats = getattr(self._pool, 'human_write_stats', None) info = super()._get_info() info.update({ 'max-concurrency': self.limit, 'processes': [p.pid for p in self._pool._pool], 'max-tasks-per-child': self._pool._maxtasksperchild or 'N/A', 'put-guarded-by-semaphore': self.putlocks, 'timeouts': (self._pool.soft_timeout or 0, self._pool.timeout or 0), 'writes': write_stats() if write_stats is not None else 'N/A', }) return info @property def num_processes(self): return self._pool._processes