Skip to content

Commit

Permalink
Kill all workers when main process exits in prefork model
Browse files Browse the repository at this point in the history
  • Loading branch information
matusvalo committed Sep 4, 2021
1 parent 917088f commit f772564
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 10 deletions.
2 changes: 2 additions & 0 deletions celery/concurrency/prefork.py
Expand Up @@ -41,6 +41,8 @@ def process_initializer(app, hostname):
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)
Expand Down
10 changes: 10 additions & 0 deletions celery/platforms.py
Expand Up @@ -17,6 +17,7 @@
from contextlib import contextmanager

from billiard.compat import close_open_fds, get_fdmax
from billiard.util import set_pdeathsig as _set_pdeathsig
# fileno used to be in this module
from kombu.utils.compat import maybe_fileno
from kombu.utils.encoding import safe_str
Expand Down Expand Up @@ -708,6 +709,15 @@ def strargv(argv):
return ''


def set_pdeathsig(name):
"""Sends signal ``name`` to process when parent process terminates."""
if signals.supported('SIGKILL'):
try:
_set_pdeathsig(signals.signum('SIGKILL'))
except OSError:
# We ignore when OS does not support set_pdeathsig
pass

def set_process_title(progname, info=None):
"""Set the :command:`ps` name for the currently running process.
Expand Down
34 changes: 25 additions & 9 deletions t/unit/concurrency/test_prefork.py
@@ -1,9 +1,11 @@
import errno
import os
import socket
import signal
from itertools import cycle
from unittest.mock import Mock, patch

from billiard.util import set_pdeathsig
import pytest
from case import mock

Expand Down Expand Up @@ -53,11 +55,18 @@ def get(self):
return self.value


@patch('celery.platforms.set_mp_process_title')
class test_process_initializer:

@staticmethod
def Loader(*args, **kwargs):
loader = Mock(*args, **kwargs)
loader.conf = {}
loader.override_backends = {}
return loader

@patch('celery.platforms.signals')
@patch('celery.platforms.set_mp_process_title')
def test_process_initializer(self, set_mp_process_title, _signals):
def test_process_initializer(self, _signals, set_mp_process_title):
with mock.restore_logging():
from celery import signals
from celery._state import _tls
Expand All @@ -67,13 +76,7 @@ def test_process_initializer(self, set_mp_process_title, _signals):
on_worker_process_init = Mock()
signals.worker_process_init.connect(on_worker_process_init)

def Loader(*args, **kwargs):
loader = Mock(*args, **kwargs)
loader.conf = {}
loader.override_backends = {}
return loader

with self.Celery(loader=Loader) as app:
with self.Celery(loader=self.Loader) as app:
app.conf = AttributeDict(DEFAULTS)
process_initializer(app, 'awesome.worker.com')
_signals.ignore.assert_any_call(*WORKER_SIGIGNORE)
Expand All @@ -100,6 +103,19 @@ def Loader(*args, **kwargs):
finally:
os.environ.pop('CELERY_LOG_FILE', None)

@patch('celery.platforms.set_pdeathsig')
def test_pdeath_sig(self, _set_pdeathsig, set_mp_process_title):
with mock.restore_logging():
from celery import signals
on_worker_process_init = Mock()
signals.worker_process_init.connect(on_worker_process_init)
from celery.concurrency.prefork import process_initializer

with self.Celery(loader=self.Loader) as app:
app.conf = AttributeDict(DEFAULTS)
process_initializer(app, 'awesome.worker.com')
_set_pdeathsig.assert_called_once_with('SIGKILL')


class test_process_destructor:

Expand Down
14 changes: 13 additions & 1 deletion t/unit/utils/test_platforms.py
Expand Up @@ -20,7 +20,7 @@
isatty, maybe_drop_privileges, parse_gid,
parse_uid, set_mp_process_title,
set_process_title, setgid, setgroups, setuid,
signals)
signals, set_pdeathsig)
from celery.utils.text import WhateverIO

try:
Expand Down Expand Up @@ -170,6 +170,18 @@ def test_setitem_raises(self, set):
signals['INT'] = lambda *a: a


class test_set_pdeathsig:

def test_call(self):
set_pdeathsig('SIGKILL')

@t.skip.if_win32
def test_call_with_correct_parameter(self):
with patch('celery.platforms._set_pdeathsig') as _set_pdeathsig:
set_pdeathsig('SIGKILL')
_set_pdeathsig.assert_called_once_with(signal.SIGKILL)


@t.skip.if_win32
class test_get_fdmax:

Expand Down

0 comments on commit f772564

Please sign in to comment.