From ea37db1410c83271e06d78a564983cba3732a1b1 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 21 Sep 2020 13:36:19 +0300
Subject: [PATCH 001/415] Specify utf-8 as the encoding for log files.
Fixes #5144.
---
celery/app/log.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/celery/app/log.py b/celery/app/log.py
index d27a85ee559..7e036746cc0 100644
--- a/celery/app/log.py
+++ b/celery/app/log.py
@@ -226,7 +226,7 @@ def _detect_handler(self, logfile=None):
logfile = sys.__stderr__ if logfile is None else logfile
if hasattr(logfile, 'write'):
return logging.StreamHandler(logfile)
- return WatchedFileHandler(logfile)
+ return WatchedFileHandler(logfile, encoding='utf-8')
def _has_handler(self, logger):
return any(
From cd8782feca5d961d08a1a46925e495b120dc3241 Mon Sep 17 00:00:00 2001
From: Akash Agrawal
Date: Wed, 30 Sep 2020 09:50:20 +0530
Subject: [PATCH 002/415] Fixed some typos in readme
---
README.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.rst b/README.rst
index cb2d07c42f9..3896f32a6fa 100644
--- a/README.rst
+++ b/README.rst
@@ -63,14 +63,14 @@ Celery version 5.0.0 runs on,
- PyPy3.6 (7.6)
-This is the next version to of celery which will support Python 3.6 or newer.
+This is the next version of celery which will support Python 3.6 or newer.
If you're running an older version of Python, you need to be running
an older version of Celery:
- Python 2.6: Celery series 3.1 or earlier.
- Python 2.5: Celery series 3.0 or earlier.
-- Python 2.4 was Celery series 2.2 or earlier.
+- Python 2.4: Celery series 2.2 or earlier.
- Python 2.7: Celery 4.x series.
Celery is a project with minimal funding,
From 0f1a53b84ab15e15bb257c4d9ce2b3459d2ed176 Mon Sep 17 00:00:00 2001
From: Michal Kuffa
Date: Tue, 29 Sep 2020 15:49:12 +0200
Subject: [PATCH 003/415] Fix custom headers propagation for protocol 1 hybrid
messages
---
celery/worker/strategy.py | 1 +
t/unit/worker/test_request.py | 4 ++--
t/unit/worker/test_strategy.py | 6 +++++-
3 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/celery/worker/strategy.py b/celery/worker/strategy.py
index 64d3c5337f2..8fb1eabd319 100644
--- a/celery/worker/strategy.py
+++ b/celery/worker/strategy.py
@@ -50,6 +50,7 @@ def hybrid_to_proto2(message, body):
'kwargsrepr': body.get('kwargsrepr'),
'origin': body.get('origin'),
}
+ headers.update(message.headers or {})
embed = {
'callbacks': body.get('callbacks'),
diff --git a/t/unit/worker/test_request.py b/t/unit/worker/test_request.py
index 039af717b2d..3ed7c553d15 100644
--- a/t/unit/worker/test_request.py
+++ b/t/unit/worker/test_request.py
@@ -1204,8 +1204,8 @@ def test_execute_using_pool_with_none_timelimit_header(self):
def test_execute_using_pool__defaults_of_hybrid_to_proto2(self):
weakref_ref = Mock(name='weakref.ref')
- headers = strategy.hybrid_to_proto2('', {'id': uuid(),
- 'task': self.mytask.name})[1]
+ headers = strategy.hybrid_to_proto2(Mock(headers=None), {'id': uuid(),
+ 'task': self.mytask.name})[1]
job = self.zRequest(revoked_tasks=set(), ref=weakref_ref, **headers)
job.execute_using_pool(self.pool)
assert job._apply_result
diff --git a/t/unit/worker/test_strategy.py b/t/unit/worker/test_strategy.py
index 6b93dab74d9..88abe4dcd27 100644
--- a/t/unit/worker/test_strategy.py
+++ b/t/unit/worker/test_strategy.py
@@ -271,7 +271,7 @@ def failed():
class test_hybrid_to_proto2:
def setup(self):
- self.message = Mock(name='message')
+ self.message = Mock(name='message', headers={"custom": "header"})
self.body = {
'args': (1,),
'kwargs': {'foo': 'baz'},
@@ -288,3 +288,7 @@ def test_retries_custom_value(self):
self.body['retries'] = _custom_value
_, headers, _, _ = hybrid_to_proto2(self.message, self.body)
assert headers.get('retries') == _custom_value
+
+ def test_custom_headers(self):
+ _, headers, _, _ = hybrid_to_proto2(self.message, self.body)
+ assert headers.get("custom") == "header"
From 6d270b94642188be774e228f97fd6af89ac547af Mon Sep 17 00:00:00 2001
From: Frazer McLean
Date: Wed, 30 Sep 2020 11:00:56 +0200
Subject: [PATCH 004/415] Retry after race during schema creation in database
backend (#6298)
* Retry after race during schema creation in database backend
Fixes #6296
This race condition does not commonly present, since the schema creation
only needs to happen once per database. It's more likely to appear in
e.g. a test suite that uses a new database each time.
For context of the sleep times I chose, the schema creation takes ~50 ms
on my laptop.
I did a simulated test run of 50 concurrent calls to MetaData.create_all
repeated 200 times and the number of retries was:
- 0 retries: 8717x
- 1 retry: 1279x
- 2 retries 4x
* Add test for prepare_models retry error condition
* Add name to contributors
---
CONTRIBUTORS.txt | 1 +
celery/backends/database/session.py | 27 ++++++++++++++++++++++++++-
t/unit/backends/test_database.py | 28 +++++++++++++++++++++++++++-
3 files changed, 54 insertions(+), 2 deletions(-)
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 748cabf4d0b..a29157e1e57 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -277,3 +277,4 @@ Kyle Johnson, 2019/09/23
Dipankar Achinta, 2019/10/24
Sardorbek Imomaliev, 2020/01/24
Maksym Shalenyi, 2020/07/30
+Frazer McLean, 2020/09/29
diff --git a/celery/backends/database/session.py b/celery/backends/database/session.py
index 047a9271d92..ca3d683bea6 100644
--- a/celery/backends/database/session.py
+++ b/celery/backends/database/session.py
@@ -1,14 +1,21 @@
"""SQLAlchemy session."""
+import time
+
from kombu.utils.compat import register_after_fork
from sqlalchemy import create_engine
+from sqlalchemy.exc import DatabaseError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import NullPool
+from celery.utils.time import get_exponential_backoff_interval
+
ResultModelBase = declarative_base()
__all__ = ('SessionManager',)
+PREPARE_MODELS_MAX_RETRIES = 10
+
def _after_fork_cleanup_session(session):
session._after_fork()
@@ -50,7 +57,25 @@ def create_session(self, dburi, short_lived_sessions=False, **kwargs):
def prepare_models(self, engine):
if not self.prepared:
- ResultModelBase.metadata.create_all(engine)
+ # SQLAlchemy will check if the items exist before trying to
+ # create them, which is a race condition. If it raises an error
+ # in one iteration, the next may pass all the existence checks
+ # and the call will succeed.
+ retries = 0
+ while True:
+ try:
+ ResultModelBase.metadata.create_all(engine)
+ except DatabaseError:
+ if retries < PREPARE_MODELS_MAX_RETRIES:
+ sleep_amount_ms = get_exponential_backoff_interval(
+ 10, retries, 1000, True
+ )
+ time.sleep(sleep_amount_ms / 1000)
+ retries += 1
+ else:
+ raise
+ else:
+ break
self.prepared = True
def session_factory(self, dburi, **kwargs):
diff --git a/t/unit/backends/test_database.py b/t/unit/backends/test_database.py
index bff42361841..28e2fedbbbb 100644
--- a/t/unit/backends/test_database.py
+++ b/t/unit/backends/test_database.py
@@ -13,7 +13,8 @@
from celery.backends.database import (DatabaseBackend, retry, session, # noqa
session_cleanup)
from celery.backends.database.models import Task, TaskSet # noqa
-from celery.backends.database.session import SessionManager # noqa
+from celery.backends.database.session import ( # noqa
+ PREPARE_MODELS_MAX_RETRIES, ResultModelBase, SessionManager)
from t import skip # noqa
@@ -398,3 +399,28 @@ def test_coverage_madness(self):
SessionManager()
finally:
session.register_after_fork = prev
+
+ @patch('celery.backends.database.session.create_engine')
+ def test_prepare_models_terminates(self, create_engine):
+ """SessionManager.prepare_models has retry logic because the creation
+ of database tables by multiple workers is racy. This test patches
+ the used method to always raise, so we can verify that it does
+ eventually terminate.
+ """
+ from sqlalchemy.dialects.sqlite import dialect
+ from sqlalchemy.exc import DatabaseError
+
+ sqlite = dialect.dbapi()
+ manager = SessionManager()
+ engine = manager.get_engine('dburi')
+
+ def raise_err(bind):
+ raise DatabaseError("", "", [], sqlite.DatabaseError)
+
+ patch_create_all = patch.object(
+ ResultModelBase.metadata, 'create_all', side_effect=raise_err)
+
+ with pytest.raises(DatabaseError), patch_create_all as mock_create_all:
+ manager.prepare_models(engine)
+
+ assert mock_create_all.call_count == PREPARE_MODELS_MAX_RETRIES + 1
From 96ec6db611f86f44a99f58d107c484dc011110ce Mon Sep 17 00:00:00 2001
From: Maarten Fonville
Date: Fri, 25 Sep 2020 23:38:56 +0200
Subject: [PATCH 005/415] Update daemonizing.rst
Fix daemonizing documentation for issue #6363 to put `multi` before `-A`
---
docs/userguide/daemonizing.rst | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/userguide/daemonizing.rst b/docs/userguide/daemonizing.rst
index 07e39009c97..225d078ac8e 100644
--- a/docs/userguide/daemonizing.rst
+++ b/docs/userguide/daemonizing.rst
@@ -72,11 +72,11 @@ the worker you must also export them (e.g., :command:`export DISPLAY=":0"`)
.. code-block:: console
- $ celery -A proj multi start worker1 \
+ $ celery multi -A proj start worker1 \
--pidfile="$HOME/run/celery/%n.pid" \
--logfile="$HOME/log/celery/%n%I.log"
- $ celery -A proj multi restart worker1 \
+ $ celery multi -A proj restart worker1 \
--logfile="$HOME/log/celery/%n%I.log" \
--pidfile="$HOME/run/celery/%n.pid
@@ -399,12 +399,12 @@ This is an example systemd file:
Group=celery
EnvironmentFile=/etc/conf.d/celery
WorkingDirectory=/opt/celery
- ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} multi start ${CELERYD_NODES} \
+ ExecStart=/bin/sh -c '${CELERY_BIN} multi -A ${CELERY_APP} start ${CELERYD_NODES} \
--pidfile=${CELERYD_PID_FILE} \
--logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS}'
ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} \
--pidfile=${CELERYD_PID_FILE}'
- ExecReload=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} multi restart ${CELERYD_NODES} \
+ ExecReload=/bin/sh -c '${CELERY_BIN} multi -A ${CELERY_APP} restart ${CELERYD_NODES} \
--pidfile=${CELERYD_PID_FILE} \
--logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS}'
@@ -492,7 +492,7 @@ This is an example systemd file for Celery Beat:
Group=celery
EnvironmentFile=/etc/conf.d/celery
WorkingDirectory=/opt/celery
- ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} beat \
+ ExecStart=/bin/sh -c '${CELERY_BIN} beat -A ${CELERY_APP} \
--pidfile=${CELERYBEAT_PID_FILE} \
--logfile=${CELERYBEAT_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
From f05e82a32a737c4222ece0b446e7fb2fd8cc883f Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 30 Sep 2020 14:07:02 +0300
Subject: [PATCH 006/415] Revert "Update daemonizing.rst" (#6376)
This reverts commit 96ec6db611f86f44a99f58d107c484dc011110ce.
---
docs/userguide/daemonizing.rst | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/userguide/daemonizing.rst b/docs/userguide/daemonizing.rst
index 225d078ac8e..07e39009c97 100644
--- a/docs/userguide/daemonizing.rst
+++ b/docs/userguide/daemonizing.rst
@@ -72,11 +72,11 @@ the worker you must also export them (e.g., :command:`export DISPLAY=":0"`)
.. code-block:: console
- $ celery multi -A proj start worker1 \
+ $ celery -A proj multi start worker1 \
--pidfile="$HOME/run/celery/%n.pid" \
--logfile="$HOME/log/celery/%n%I.log"
- $ celery multi -A proj restart worker1 \
+ $ celery -A proj multi restart worker1 \
--logfile="$HOME/log/celery/%n%I.log" \
--pidfile="$HOME/run/celery/%n.pid
@@ -399,12 +399,12 @@ This is an example systemd file:
Group=celery
EnvironmentFile=/etc/conf.d/celery
WorkingDirectory=/opt/celery
- ExecStart=/bin/sh -c '${CELERY_BIN} multi -A ${CELERY_APP} start ${CELERYD_NODES} \
+ ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} multi start ${CELERYD_NODES} \
--pidfile=${CELERYD_PID_FILE} \
--logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS}'
ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} \
--pidfile=${CELERYD_PID_FILE}'
- ExecReload=/bin/sh -c '${CELERY_BIN} multi -A ${CELERY_APP} restart ${CELERYD_NODES} \
+ ExecReload=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} multi restart ${CELERYD_NODES} \
--pidfile=${CELERYD_PID_FILE} \
--logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS}'
@@ -492,7 +492,7 @@ This is an example systemd file for Celery Beat:
Group=celery
EnvironmentFile=/etc/conf.d/celery
WorkingDirectory=/opt/celery
- ExecStart=/bin/sh -c '${CELERY_BIN} beat -A ${CELERY_APP} \
+ ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} beat \
--pidfile=${CELERYBEAT_PID_FILE} \
--logfile=${CELERYBEAT_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
From ce4f759a5766331285c779ed87b724a755d18b74 Mon Sep 17 00:00:00 2001
From: laixintao
Date: Wed, 30 Sep 2020 21:36:09 +0800
Subject: [PATCH 007/415] bugfix: when set config result_expires = 0, chord.get
will hang. (#6373)
* bugfix: when set config result_expires = 0, chord.get will hang.
`EXPIRE key 0` will delete a key in redis, then chord will never get the
result.
fix: https://github.com/celery/celery/issues/5237
* test: add testcase for expire when set config with zero.
---
celery/backends/redis.py | 2 +-
t/unit/backends/test_redis.py | 18 ++++++++++++++++++
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index 1b9db7433fe..2c428823538 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -436,7 +436,7 @@ def on_chord_part_return(self, request, state, result,
if self._chord_zset
else pipe.rpush(jkey, encoded).llen(jkey)
).get(tkey)
- if self.expires is not None:
+ if self.expires:
pipeline = pipeline \
.expire(jkey, self.expires) \
.expire(tkey, self.expires)
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index 3f6257c8ae7..2029edc3c29 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -712,6 +712,24 @@ def test_on_chord_part_return_no_expiry(self, restore):
self.b.expires = old_expires
+ @patch('celery.result.GroupResult.restore')
+ def test_on_chord_part_return_expire_set_to_zero(self, restore):
+ old_expires = self.b.expires
+ self.b.expires = 0
+ tasks = [self.create_task(i) for i in range(10)]
+
+ for i in range(10):
+ self.b.on_chord_part_return(tasks[i].request, states.SUCCESS, i)
+ assert self.b.client.zadd.call_count
+ self.b.client.zadd.reset_mock()
+ assert self.b.client.zrangebyscore.call_count
+ jkey = self.b.get_key_for_group('group_id', '.j')
+ tkey = self.b.get_key_for_group('group_id', '.t')
+ self.b.client.delete.assert_has_calls([call(jkey), call(tkey)])
+ self.b.client.expire.assert_not_called()
+
+ self.b.expires = old_expires
+
@patch('celery.result.GroupResult.restore')
def test_on_chord_part_return_no_expiry__unordered(self, restore):
self.app.conf.result_backend_transport_options = dict(
From 431fffd7f29824cc08d566ed40bf398579979820 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 30 Sep 2020 15:16:20 +0300
Subject: [PATCH 008/415] Display a custom error message whenever an attempt to
use -A or --app as a sub-command option was made.
Fixes #6363
---
celery/bin/base.py | 5 ++---
celery/bin/celery.py | 30 +++++++++++++++++++++++++++++-
2 files changed, 31 insertions(+), 4 deletions(-)
diff --git a/celery/bin/base.py b/celery/bin/base.py
index 9429900a957..662ba728ae9 100644
--- a/celery/bin/base.py
+++ b/celery/bin/base.py
@@ -39,8 +39,7 @@ def __init__(self, app, no_color, workdir, quiet=False):
@cached_property
def OK(self):
- return self.style("OK", fg="green", bold=True) \
-
+ return self.style("OK", fg="green", bold=True)
@cached_property
def ERROR(self):
@@ -72,7 +71,7 @@ def error(self, message=None, **kwargs):
kwargs['color'] = False
click.echo(message, **kwargs)
else:
- click.echo(message, **kwargs)
+ click.secho(message, **kwargs)
def pretty(self, n):
if isinstance(n, list):
diff --git a/celery/bin/celery.py b/celery/bin/celery.py
index 4f7c95d065c..9f4fa0cbe4c 100644
--- a/celery/bin/celery.py
+++ b/celery/bin/celery.py
@@ -2,6 +2,7 @@
import os
import click
+import click.exceptions
from click.types import ParamType
from click_didyoumean import DYMGroup
@@ -104,7 +105,8 @@ def celery(ctx, app, broker, result_backend, loader, config, workdir,
os.environ['CELERY_RESULT_BACKEND'] = result_backend
if config:
os.environ['CELERY_CONFIG_MODULE'] = config
- ctx.obj = CLIContext(app=app, no_color=no_color, workdir=workdir, quiet=quiet)
+ ctx.obj = CLIContext(app=app, no_color=no_color, workdir=workdir,
+ quiet=quiet)
# User options
worker.params.extend(ctx.obj.app.user_options.get('worker', []))
@@ -139,6 +141,32 @@ def report(ctx):
celery.add_command(shell)
celery.add_command(multi)
+# Monkey-patch click to display a custom error
+# when -A or --app are used as sub-command options instead of as options
+# of the global command.
+
+previous_show_implementation = click.exceptions.NoSuchOption.show
+
+WRONG_APP_OPTION_USAGE_MESSAGE = """You are using `{option_name}` as an option of the {info_name} sub-command:
+celery {info_name} {option_name} celeryapp <...>
+
+The support for this usage was removed in Celery 5.0. Instead you should use `{option_name}` as a global option:
+celery {option_name} celeryapp {info_name} <...>"""
+
+
+def _show(self, file=None):
+ if self.option_name in ('-A', '--app'):
+ self.ctx.obj.error(
+ WRONG_APP_OPTION_USAGE_MESSAGE.format(
+ option_name=self.option_name,
+ info_name=self.ctx.info_name),
+ fg='red'
+ )
+ previous_show_implementation(self, file=file)
+
+
+click.exceptions.NoSuchOption.show = _show
+
def main() -> int:
"""Start celery umbrella command.
From c41a5cfe363e6359aebcce553f02d11803e0ead0 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 1 Oct 2020 12:28:50 +0300
Subject: [PATCH 009/415] Remove test dependencies for Python 2.7.
---
requirements/test.txt | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/requirements/test.txt b/requirements/test.txt
index fd0ba172f90..8d338510e71 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,10 +1,8 @@
case>=1.3.1
-pytest~=4.6; python_version < '3.0'
-pytest~=6.0; python_version >= '3.0'
+pytest~=6.0
pytest-celery
pytest-timeout~=1.4.2
boto3>=1.9.178
-python-dateutil<2.8.1,>=2.1; python_version < '3.0'
moto==1.3.7
pre-commit
-r extras/yaml.txt
From 86e0d933ecaf588fee1903708c413edb3188dd24 Mon Sep 17 00:00:00 2001
From: Nicolas Dandrimont
Date: Thu, 1 Oct 2020 15:27:31 +0200
Subject: [PATCH 010/415] Restore the celery worker
--without-{gossip,mingle,heartbeat} flags (#6365)
In the previously used argparse arguments framework, these three options were
used as flags.
Since 5.0.0, they are options which need to take an argument (whose only
sensible value would be "true"). The error message coming up is also (very)
hard to understand, when running the celery worker command with an odd number
of flags:
Error: Unable to parse extra configuration from command line.
Reason: not enough values to unpack (expected 2, got 1)
When the celery worker is run with an even number of flags, the last one is
considered as an argument of the previous one, which is a subtle bug.
---
celery/bin/worker.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/celery/bin/worker.py b/celery/bin/worker.py
index 4d4c57aea16..834a01bdae2 100644
--- a/celery/bin/worker.py
+++ b/celery/bin/worker.py
@@ -231,15 +231,15 @@ def detach(path, argv, logfile=None, pidfile=None, uid=None,
cls=CeleryOption,
help_group="Queue Options")
@click.option('--without-gossip',
- default=False,
+ is_flag=True,
cls=CeleryOption,
help_group="Features")
@click.option('--without-mingle',
- default=False,
+ is_flag=True,
cls=CeleryOption,
help_group="Features")
@click.option('--without-heartbeat',
- default=False,
+ is_flag=True,
cls=CeleryOption,
help_group="Features", )
@click.option('--heartbeat-interval',
From 8767df022a9175db3520c0dfb5d3d562711383f2 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 1 Oct 2020 14:23:31 +0300
Subject: [PATCH 011/415] Provide clearer error messages when app fails to
load.
---
celery/bin/celery.py | 31 +++++++++++++++++++++++++++++--
1 file changed, 29 insertions(+), 2 deletions(-)
diff --git a/celery/bin/celery.py b/celery/bin/celery.py
index 9f4fa0cbe4c..5488d17c40e 100644
--- a/celery/bin/celery.py
+++ b/celery/bin/celery.py
@@ -1,5 +1,6 @@
"""Celery Command Line Interface."""
import os
+import traceback
import click
import click.exceptions
@@ -25,6 +26,19 @@
from celery.bin.upgrade import upgrade
from celery.bin.worker import worker
+UNABLE_TO_LOAD_APP_MODULE_NOT_FOUND = click.style("""
+Unable to load celery application.
+The module {0} was not found.""", fg='red')
+
+UNABLE_TO_LOAD_APP_ERROR_OCCURRED = click.style("""
+Unable to load celery application.
+While trying to load the module {0} the following error occurred:
+{1}""", fg='red')
+
+UNABLE_TO_LOAD_APP_APP_MISSING = click.style("""
+Unable to load celery application.
+{0}""")
+
class App(ParamType):
"""Application option."""
@@ -34,8 +48,21 @@ class App(ParamType):
def convert(self, value, param, ctx):
try:
return find_app(value)
- except (ModuleNotFoundError, AttributeError) as e:
- self.fail(str(e))
+ except ModuleNotFoundError as e:
+ if e.name != value:
+ exc = traceback.format_exc()
+ self.fail(
+ UNABLE_TO_LOAD_APP_ERROR_OCCURRED.format(value, exc)
+ )
+ self.fail(UNABLE_TO_LOAD_APP_MODULE_NOT_FOUND.format(e.name))
+ except AttributeError as e:
+ attribute_name = e.args[0].capitalize()
+ self.fail(UNABLE_TO_LOAD_APP_APP_MISSING.format(attribute_name))
+ except Exception:
+ exc = traceback.format_exc()
+ self.fail(
+ UNABLE_TO_LOAD_APP_ERROR_OCCURRED.format(value, exc)
+ )
APP = App()
From 7288147d65a32b726869ed887d99e4bfd8c070e2 Mon Sep 17 00:00:00 2001
From: Thomas Grainger
Date: Sun, 4 Oct 2020 14:02:47 +0100
Subject: [PATCH 012/415] fix pytest plugin registration documentation (#6387)
* fix pytest plugin registration documentation
* Update docs/userguide/testing.rst
Co-authored-by: Thomas Grainger
Co-authored-by: Omer Katz
---
docs/userguide/testing.rst | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/userguide/testing.rst b/docs/userguide/testing.rst
index 4deccd0f15c..330a24d1dc2 100644
--- a/docs/userguide/testing.rst
+++ b/docs/userguide/testing.rst
@@ -105,7 +105,8 @@ Celery initially ships the plugin in a disabled state, to enable it you can eith
* `pip install celery[pytest]`
* `pip install pytest-celery`
- * or add `pytest_plugins = 'celery.contrib.pytest'` to your pytest.ini
+ * or add an environment variable `PYTEST_PLUGINS=celery.contrib.pytest`
+ * or add `pytest_plugins = ("celery.contrib.pytest", )` to your root conftest.py
Marks
From 243d475b199f13ac2aa85d4225abd3a094ae781f Mon Sep 17 00:00:00 2001
From: Bas ten Berge
Date: Mon, 5 Oct 2020 07:09:29 +0200
Subject: [PATCH 013/415] Contains a workaround for the capitalized
configuration issue (#6385)
* Contains a workaround for the capitalized configuration issue
* Update celery/apps/worker.py
Co-authored-by: Omer Katz
* Update celery/apps/worker.py
Co-authored-by: Omer Katz
Co-authored-by: Omer Katz
---
celery/apps/worker.py | 25 +++++++++++++++++++------
1 file changed, 19 insertions(+), 6 deletions(-)
diff --git a/celery/apps/worker.py b/celery/apps/worker.py
index 6c1b5eb1c20..2a9df0c2e79 100644
--- a/celery/apps/worker.py
+++ b/celery/apps/worker.py
@@ -141,12 +141,25 @@ def on_start(self):
app.log.redirect_stdouts(self.redirect_stdouts_level)
# TODO: Remove the following code in Celery 6.0
- if app.conf.maybe_warn_deprecated_settings():
- logger.warning(
- "Please run `celery upgrade settings path/to/settings.py` "
- "to avoid these warnings and to allow a smoother upgrade "
- "to Celery 6.0."
- )
+ # This qualifies as a hack for issue #6366.
+ # a hack via app.__reduce_keys__(), but that may not work properly in
+ # all cases
+ warn_deprecated = True
+ config_source = app._config_source
+ if isinstance(config_source, str):
+ # Don't raise the warning when the settings originate from
+ # django.conf:settings
+ warn_deprecated = config_source.lower() not in [
+ 'django.conf:settings',
+ ]
+
+ if warn_deprecated:
+ if app.conf.maybe_warn_deprecated_settings():
+ logger.warning(
+ "Please run `celery upgrade settings path/to/settings.py` "
+ "to avoid these warnings and to allow a smoother upgrade "
+ "to Celery 6.0."
+ )
def emit_banner(self):
# Dump configuration to screen so we have some basic information
From 9eac689aa904e88b8327122629538980cd4ef6c9 Mon Sep 17 00:00:00 2001
From: Zvi Baratz
Date: Mon, 5 Oct 2020 18:09:18 +0300
Subject: [PATCH 014/415] Remove old explanation regarding `absolute_import`
(#6390)
Resolves #6389.
---
docs/django/first-steps-with-django.rst | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/docs/django/first-steps-with-django.rst b/docs/django/first-steps-with-django.rst
index 003edcc8b06..956d965313b 100644
--- a/docs/django/first-steps-with-django.rst
+++ b/docs/django/first-steps-with-django.rst
@@ -54,15 +54,8 @@ for simple projects you may use a single contained module that defines
both the app and tasks, like in the :ref:`tut-celery` tutorial.
Let's break down what happens in the first module,
-first we import absolute imports from the future, so that our
-``celery.py`` module won't clash with the library:
-
-.. code-block:: python
-
- from __future__ import absolute_import
-
-Then we set the default :envvar:`DJANGO_SETTINGS_MODULE` environment variable
-for the :program:`celery` command-line program:
+first, we set the default :envvar:`DJANGO_SETTINGS_MODULE` environment
+variable for the :program:`celery` command-line program:
.. code-block:: python
From 66d2ea51ca8dff22dc11e6fd6119a3beedd83b51 Mon Sep 17 00:00:00 2001
From: Zvi Baratz
Date: Tue, 6 Oct 2020 18:07:54 +0300
Subject: [PATCH 015/415] Update canvas.rst (#6392)
* Update canvas.rst
Tiny fixes.
* Update docs/userguide/canvas.rst
Co-authored-by: Omer Katz
Co-authored-by: Omer Katz
---
docs/userguide/canvas.rst | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/userguide/canvas.rst b/docs/userguide/canvas.rst
index fdfcaf2719a..10240768435 100644
--- a/docs/userguide/canvas.rst
+++ b/docs/userguide/canvas.rst
@@ -959,11 +959,11 @@ Map & Starmap
-------------
:class:`~celery.map` and :class:`~celery.starmap` are built-in tasks
-that calls the task for every element in a sequence.
+that call the provided calling task for every element in a sequence.
-They differ from group in that
+They differ from :class:`~celery.group` in that:
-- only one task message is sent
+- only one task message is sent.
- the operation is sequential.
@@ -1013,7 +1013,7 @@ Chunks
------
Chunking lets you divide an iterable of work into pieces, so that if
-you have one million objects, you can create 10 tasks with hundred
+you have one million objects, you can create 10 tasks with a hundred
thousand objects each.
Some may worry that chunking your tasks results in a degradation
From 8af82d7ed8625250907af268e7696e43570b2ac6 Mon Sep 17 00:00:00 2001
From: Justinas Petuchovas
Date: Wed, 7 Oct 2020 15:01:39 +0300
Subject: [PATCH 016/415] Remove duplicate words from docs (#6398)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Remove the duplicate usage of “required” in documentation (specifically, `introduction.rst`).
---
docs/getting-started/introduction.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/getting-started/introduction.rst b/docs/getting-started/introduction.rst
index ea2162467ae..f55f448da79 100644
--- a/docs/getting-started/introduction.rst
+++ b/docs/getting-started/introduction.rst
@@ -45,7 +45,7 @@ What do I need?
- PyPy3.6 ❨7.3❩
Celery 4.x was the last version to support Python 2.7,
- Celery 5.x requires Python 3.6 or newer is required.
+ Celery 5.x requires Python 3.6 or newer.
If you're running an older version of Python, you need to be running
an older version of Celery:
From 08fb1d06cff06397f365c546032479b8d9925931 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 7 Oct 2020 16:06:25 +0300
Subject: [PATCH 017/415] Allow lowercase log levels. (#6396)
Fixes #6395.
---
celery/bin/base.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/celery/bin/base.py b/celery/bin/base.py
index 662ba728ae9..fbb56d84dbb 100644
--- a/celery/bin/base.py
+++ b/celery/bin/base.py
@@ -220,6 +220,7 @@ def __init__(self):
super().__init__(('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'FATAL'))
def convert(self, value, param, ctx):
+ value = value.upper()
value = super().convert(value, param, ctx)
return mlevel(value)
From 844774b76b8e434d5b88349be41c7c578b6fa48e Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 7 Oct 2020 16:07:44 +0300
Subject: [PATCH 018/415] Detach now correctly passes options with more than
one word. (#6394)
When specifying options such as `-E` the detached worker should receive the `--task-events` option.
Instead it got the `--task_events` option which doesn't exist and therefore silently failed.
This fixes #6362.
---
celery/bin/worker.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/celery/bin/worker.py b/celery/bin/worker.py
index 834a01bdae2..bf58dbea647 100644
--- a/celery/bin/worker.py
+++ b/celery/bin/worker.py
@@ -302,6 +302,7 @@ def worker(ctx, hostname=None, pool_cls=None, app=None, uid=None, gid=None,
executable = params.pop('executable')
argv = ['-m', 'celery', 'worker']
for arg, value in params.items():
+ arg = arg.replace("_", "-")
if isinstance(value, bool) and value:
argv.append(f'--{arg}')
else:
From 8a92b7128bc921d4332fe01486accc243115aba8 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 7 Oct 2020 16:11:42 +0300
Subject: [PATCH 019/415] The celery multi command now works as expected.
(#6388)
---
celery/apps/multi.py | 26 +++++++++++++++++++++---
celery/bin/multi.py | 7 ++++++-
requirements/test-ci-default.txt | 2 +-
t/unit/apps/test_multi.py | 35 ++++++++++++++++----------------
4 files changed, 47 insertions(+), 23 deletions(-)
diff --git a/celery/apps/multi.py b/celery/apps/multi.py
index b82eee4c9b3..a1458e3bd63 100644
--- a/celery/apps/multi.py
+++ b/celery/apps/multi.py
@@ -150,7 +150,7 @@ def _setdefaultopt(self, d, alt, value):
pass
value = d.setdefault(alt[0], os.path.normpath(value))
dir_path = os.path.dirname(value)
- if not os.path.exists(dir_path):
+ if dir_path and not os.path.exists(dir_path):
os.makedirs(dir_path)
return value
@@ -160,10 +160,30 @@ def _prepare_expander(self):
self.name, shortname, hostname)
def _prepare_argv(self):
+ cmd = self.expander(self.cmd).split(' ')
+ i = cmd.index('celery') + 1
+
+ options = self.options.copy()
+ for opt, value in self.options.items():
+ if opt in (
+ '-A', '--app',
+ '-b', '--broker',
+ '--result-backend',
+ '--loader',
+ '--config',
+ '--workdir',
+ '-C', '--no-color',
+ '-q', '--quiet',
+ ):
+ cmd.insert(i, format_opt(opt, self.expander(value)))
+
+ options.pop(opt)
+
+ cmd = [' '.join(cmd)]
argv = tuple(
- [self.expander(self.cmd)] +
+ cmd +
[format_opt(opt, self.expander(value))
- for opt, value in self.options.items()] +
+ for opt, value in options.items()] +
[self.extra_args]
)
if self.append:
diff --git a/celery/bin/multi.py b/celery/bin/multi.py
index 3e999ab2ab5..12bb52b87d2 100644
--- a/celery/bin/multi.py
+++ b/celery/bin/multi.py
@@ -471,4 +471,9 @@ def DOWN(self):
def multi(ctx):
"""Start multiple worker instances."""
cmd = MultiTool(quiet=ctx.obj.quiet, no_color=ctx.obj.no_color)
- return cmd.execute_from_commandline([''] + ctx.args)
+ # In 4.x, celery multi ignores the global --app option.
+ # Since in 5.0 the --app option is global only we
+ # rearrange the arguments so that the MultiTool will parse them correctly.
+ args = sys.argv[1:]
+ args = args[args.index('multi'):] + args[:args.index('multi')]
+ return cmd.execute_from_commandline(args)
diff --git a/requirements/test-ci-default.txt b/requirements/test-ci-default.txt
index 953ed9aecc7..fdcf4684733 100644
--- a/requirements/test-ci-default.txt
+++ b/requirements/test-ci-default.txt
@@ -12,7 +12,7 @@
-r extras/thread.txt
-r extras/elasticsearch.txt
-r extras/couchdb.txt
--r extras/couchbase.txt
+#-r extras/couchbase.txt
-r extras/arangodb.txt
-r extras/consul.txt
-r extras/cosmosdbsql.txt
diff --git a/t/unit/apps/test_multi.py b/t/unit/apps/test_multi.py
index f7de1d5e27f..4c3fd9bfc1f 100644
--- a/t/unit/apps/test_multi.py
+++ b/t/unit/apps/test_multi.py
@@ -69,7 +69,7 @@ def test_parse(self, gethostname, mkdirs_mock):
'--', '.disable_rate_limits=1',
])
p.parse()
- it = multi_args(p, cmd='COMMAND', append='*AP*',
+ it = multi_args(p, cmd='celery multi', append='*AP*',
prefix='*P*', suffix='*S*')
nodes = list(it)
@@ -85,32 +85,32 @@ def assert_line_in(name, args):
assert_line_in(
'*P*jerry@*S*',
- ['COMMAND', '-n *P*jerry@*S*', '-Q bar',
+ ['celery multi', '-n *P*jerry@*S*', '-Q bar',
'-c 5', '--flag', '--logfile=/var/log/celery/foo',
'-- .disable_rate_limits=1', '*AP*'],
)
assert_line_in(
'*P*elaine@*S*',
- ['COMMAND', '-n *P*elaine@*S*', '-Q bar',
+ ['celery multi', '-n *P*elaine@*S*', '-Q bar',
'-c 5', '--flag', '--logfile=/var/log/celery/foo',
'-- .disable_rate_limits=1', '*AP*'],
)
assert_line_in(
'*P*kramer@*S*',
- ['COMMAND', '--loglevel=DEBUG', '-n *P*kramer@*S*',
+ ['celery multi', '--loglevel=DEBUG', '-n *P*kramer@*S*',
'-Q bar', '--flag', '--logfile=/var/log/celery/foo',
'-- .disable_rate_limits=1', '*AP*'],
)
expand = nodes[0].expander
assert expand('%h') == '*P*jerry@*S*'
assert expand('%n') == '*P*jerry'
- nodes2 = list(multi_args(p, cmd='COMMAND', append='',
+ nodes2 = list(multi_args(p, cmd='celery multi', append='',
prefix='*P*', suffix='*S*'))
assert nodes2[0].argv[-1] == '-- .disable_rate_limits=1'
p2 = NamespacedOptionParser(['10', '-c:1', '5'])
p2.parse()
- nodes3 = list(multi_args(p2, cmd='COMMAND'))
+ nodes3 = list(multi_args(p2, cmd='celery multi'))
def _args(name, *args):
return args + (
@@ -123,40 +123,40 @@ def _args(name, *args):
assert len(nodes3) == 10
assert nodes3[0].name == 'celery1@example.com'
assert nodes3[0].argv == (
- 'COMMAND', '-c 5', '-n celery1@example.com') + _args('celery1')
+ 'celery multi', '-c 5', '-n celery1@example.com') + _args('celery1')
for i, worker in enumerate(nodes3[1:]):
assert worker.name == 'celery%s@example.com' % (i + 2)
node_i = f'celery{i + 2}'
assert worker.argv == (
- 'COMMAND',
+ 'celery multi',
f'-n {node_i}@example.com') + _args(node_i)
- nodes4 = list(multi_args(p2, cmd='COMMAND', suffix='""'))
+ nodes4 = list(multi_args(p2, cmd='celery multi', suffix='""'))
assert len(nodes4) == 10
assert nodes4[0].name == 'celery1@'
assert nodes4[0].argv == (
- 'COMMAND', '-c 5', '-n celery1@') + _args('celery1')
+ 'celery multi', '-c 5', '-n celery1@') + _args('celery1')
p3 = NamespacedOptionParser(['foo@', '-c:foo', '5'])
p3.parse()
- nodes5 = list(multi_args(p3, cmd='COMMAND', suffix='""'))
+ nodes5 = list(multi_args(p3, cmd='celery multi', suffix='""'))
assert nodes5[0].name == 'foo@'
assert nodes5[0].argv == (
- 'COMMAND', '-c 5', '-n foo@') + _args('foo')
+ 'celery multi', '-c 5', '-n foo@') + _args('foo')
p4 = NamespacedOptionParser(['foo', '-Q:1', 'test'])
p4.parse()
- nodes6 = list(multi_args(p4, cmd='COMMAND', suffix='""'))
+ nodes6 = list(multi_args(p4, cmd='celery multi', suffix='""'))
assert nodes6[0].name == 'foo@'
assert nodes6[0].argv == (
- 'COMMAND', '-Q test', '-n foo@') + _args('foo')
+ 'celery multi', '-Q test', '-n foo@') + _args('foo')
p5 = NamespacedOptionParser(['foo@bar', '-Q:1', 'test'])
p5.parse()
- nodes7 = list(multi_args(p5, cmd='COMMAND', suffix='""'))
+ nodes7 = list(multi_args(p5, cmd='celery multi', suffix='""'))
assert nodes7[0].name == 'foo@bar'
assert nodes7[0].argv == (
- 'COMMAND', '-Q test', '-n foo@bar') + _args('foo')
+ 'celery multi', '-Q test', '-n foo@bar') + _args('foo')
p6 = NamespacedOptionParser(['foo@bar', '-Q:0', 'test'])
p6.parse()
@@ -192,8 +192,7 @@ def test_from_kwargs(self):
max_tasks_per_child=30, A='foo', Q='q1,q2', O='fair',
)
assert sorted(n.argv) == sorted([
- '-m celery worker --detach',
- '-A foo',
+ '-m celery -A foo worker --detach',
f'--executable={n.executable}',
'-O fair',
'-n foo@bar.com',
From b81ac620800a914eba9e18825b86325709c59421 Mon Sep 17 00:00:00 2001
From: bastb
Date: Sun, 11 Oct 2020 16:17:41 +0200
Subject: [PATCH 020/415] Contains the missed change requested by @thedrow
---
celery/apps/worker.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/celery/apps/worker.py b/celery/apps/worker.py
index 2a9df0c2e79..882751fb8a9 100644
--- a/celery/apps/worker.py
+++ b/celery/apps/worker.py
@@ -142,8 +142,6 @@ def on_start(self):
# TODO: Remove the following code in Celery 6.0
# This qualifies as a hack for issue #6366.
- # a hack via app.__reduce_keys__(), but that may not work properly in
- # all cases
warn_deprecated = True
config_source = app._config_source
if isinstance(config_source, str):
From c508296ce64b54578d37a66bc8e34ccba667e2e2 Mon Sep 17 00:00:00 2001
From: Zvi Baratz
Date: Sat, 10 Oct 2020 12:45:13 +0300
Subject: [PATCH 021/415] Added a some celery configuration examples.
---
docs/django/first-steps-with-django.rst | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/docs/django/first-steps-with-django.rst b/docs/django/first-steps-with-django.rst
index 956d965313b..ce48203d66c 100644
--- a/docs/django/first-steps-with-django.rst
+++ b/docs/django/first-steps-with-django.rst
@@ -54,7 +54,7 @@ for simple projects you may use a single contained module that defines
both the app and tasks, like in the :ref:`tut-celery` tutorial.
Let's break down what happens in the first module,
-first, we set the default :envvar:`DJANGO_SETTINGS_MODULE` environment
+first, we set the default :envvar:`DJANGO_SETTINGS_MODULE` environment
variable for the :program:`celery` command-line program:
.. code-block:: python
@@ -90,6 +90,18 @@ setting becomes ``CELERY_BROKER_URL``. This also applies to the
workers settings, for instance, the :setting:`worker_concurrency`
setting becomes ``CELERY_WORKER_CONCURRENCY``.
+For example, a Django project's configuration file might include:
+
+.. code-block:: python
+ :caption: settings.py
+
+ ...
+
+ # Celery Configuration Options
+ CELERY_TIMEZONE = "Australia/Tasmania"
+ CELERY_TASK_TRACK_STARTED = True
+ CELERY_TASK_TIME_LIMIT = 30 * 60
+
You can pass the settings object directly instead, but using a string
is better since then the worker doesn't have to serialize the object.
The ``CELERY_`` namespace is also optional, but recommended (to
From 70cbed3e2f6992ecbbc9257f60a9d251a6dceaf6 Mon Sep 17 00:00:00 2001
From: Artem Bernatskyi
Date: Mon, 12 Oct 2020 16:37:23 +0300
Subject: [PATCH 022/415] fixed loglevel info->INFO in docs
---
docs/django/first-steps-with-django.rst | 2 +-
docs/getting-started/next-steps.rst | 14 +++++++-------
docs/userguide/application.rst | 4 ++--
docs/userguide/calling.rst | 2 +-
docs/userguide/debugging.rst | 2 +-
docs/userguide/workers.rst | 10 +++++-----
examples/app/myapp.py | 6 +++---
examples/django/README.rst | 2 +-
examples/eventlet/README.rst | 2 +-
examples/periodic-tasks/myapp.py | 8 ++++----
examples/security/mysecureapp.py | 2 +-
11 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/docs/django/first-steps-with-django.rst b/docs/django/first-steps-with-django.rst
index ce48203d66c..55d64c990eb 100644
--- a/docs/django/first-steps-with-django.rst
+++ b/docs/django/first-steps-with-django.rst
@@ -247,7 +247,7 @@ development it is useful to be able to start a worker instance by using the
.. code-block:: console
- $ celery -A proj worker -l info
+ $ celery -A proj worker -l INFO
For a complete listing of the command-line options available,
use the help command:
diff --git a/docs/getting-started/next-steps.rst b/docs/getting-started/next-steps.rst
index 1cf0b35f714..2b66fd5ce04 100644
--- a/docs/getting-started/next-steps.rst
+++ b/docs/getting-started/next-steps.rst
@@ -74,7 +74,7 @@ The :program:`celery` program can be used to start the worker (you need to run t
.. code-block:: console
- $ celery -A proj worker -l info
+ $ celery -A proj worker -l INFO
When the worker starts you should see a banner and some messages::
@@ -152,7 +152,7 @@ start one or more workers in the background:
.. code-block:: console
- $ celery multi start w1 -A proj -l info
+ $ celery multi start w1 -A proj -l INFO
celery multi v4.0.0 (latentcall)
> Starting nodes...
> w1.halcyon.local: OK
@@ -161,7 +161,7 @@ You can restart it too:
.. code-block:: console
- $ celery multi restart w1 -A proj -l info
+ $ celery multi restart w1 -A proj -l INFO
celery multi v4.0.0 (latentcall)
> Stopping nodes...
> w1.halcyon.local: TERM -> 64024
@@ -176,7 +176,7 @@ or stop it:
.. code-block:: console
- $ celery multi stop w1 -A proj -l info
+ $ celery multi stop w1 -A proj -l INFO
The ``stop`` command is asynchronous so it won't wait for the
worker to shutdown. You'll probably want to use the ``stopwait`` command
@@ -185,7 +185,7 @@ before exiting:
.. code-block:: console
- $ celery multi stopwait w1 -A proj -l info
+ $ celery multi stopwait w1 -A proj -l INFO
.. note::
@@ -202,7 +202,7 @@ you're encouraged to put these in a dedicated directory:
$ mkdir -p /var/run/celery
$ mkdir -p /var/log/celery
- $ celery multi start w1 -A proj -l info --pidfile=/var/run/celery/%n.pid \
+ $ celery multi start w1 -A proj -l INFO --pidfile=/var/run/celery/%n.pid \
--logfile=/var/log/celery/%n%I.log
With the multi command you can start multiple workers, and there's a powerful
@@ -211,7 +211,7 @@ for example:
.. code-block:: console
- $ celery multi start 10 -A proj -l info -Q:1-3 images,video -Q:4,5 data \
+ $ celery multi start 10 -A proj -l INFO -Q:1-3 images,video -Q:4,5 data \
-Q default -L:4,5 debug
For more examples see the :mod:`~celery.bin.multi` module in the API
diff --git a/docs/userguide/application.rst b/docs/userguide/application.rst
index 1e6c4cf13ae..6ec6c7f8f89 100644
--- a/docs/userguide/application.rst
+++ b/docs/userguide/application.rst
@@ -257,7 +257,7 @@ You can then specify the configuration module to use via the environment:
.. code-block:: console
- $ CELERY_CONFIG_MODULE="celeryconfig.prod" celery worker -l info
+ $ CELERY_CONFIG_MODULE="celeryconfig.prod" celery worker -l INFO
.. _app-censored-config:
@@ -431,7 +431,7 @@ chain breaks:
.. code-block:: console
- $ CELERY_TRACE_APP=1 celery worker -l info
+ $ CELERY_TRACE_APP=1 celery worker -l INFO
.. topic:: Evolving the API
diff --git a/docs/userguide/calling.rst b/docs/userguide/calling.rst
index 04c7f9ba718..811820b44a1 100644
--- a/docs/userguide/calling.rst
+++ b/docs/userguide/calling.rst
@@ -692,7 +692,7 @@ the workers :option:`-Q ` argument:
.. code-block:: console
- $ celery -A proj worker -l info -Q celery,priority.high
+ $ celery -A proj worker -l INFO -Q celery,priority.high
.. seealso::
diff --git a/docs/userguide/debugging.rst b/docs/userguide/debugging.rst
index 4eeb539be36..690e2acb4bd 100644
--- a/docs/userguide/debugging.rst
+++ b/docs/userguide/debugging.rst
@@ -110,7 +110,7 @@ For example starting the worker with:
.. code-block:: console
- $ CELERY_RDBSIG=1 celery worker -l info
+ $ CELERY_RDBSIG=1 celery worker -l INFO
You can start an rdb session for any of the worker processes by executing:
diff --git a/docs/userguide/workers.rst b/docs/userguide/workers.rst
index 098d3005f68..aec8c9e5414 100644
--- a/docs/userguide/workers.rst
+++ b/docs/userguide/workers.rst
@@ -23,7 +23,7 @@ You can start the worker in the foreground by executing the command:
.. code-block:: console
- $ celery -A proj worker -l info
+ $ celery -A proj worker -l INFO
For a full list of available command-line options see
:mod:`~celery.bin.worker`, or simply do:
@@ -108,7 +108,7 @@ is by using `celery multi`:
.. code-block:: console
- $ celery multi start 1 -A proj -l info -c4 --pidfile=/var/run/celery/%n.pid
+ $ celery multi start 1 -A proj -l INFO -c4 --pidfile=/var/run/celery/%n.pid
$ celery multi restart 1 --pidfile=/var/run/celery/%n.pid
For production deployments you should be using init-scripts or a process
@@ -410,7 +410,7 @@ argument to :program:`celery worker`:
.. code-block:: console
- $ celery -A proj worker -l info --statedb=/var/run/celery/worker.state
+ $ celery -A proj worker -l INFO --statedb=/var/run/celery/worker.state
or if you use :program:`celery multi` you want to create one file per
worker instance so use the `%n` format to expand the current node
@@ -418,7 +418,7 @@ name:
.. code-block:: console
- celery multi start 2 -l info --statedb=/var/run/celery/%n.state
+ celery multi start 2 -l INFO --statedb=/var/run/celery/%n.state
See also :ref:`worker-files`
@@ -611,7 +611,7 @@ separated list of queues to the :option:`-Q ` option:
.. code-block:: console
- $ celery -A proj worker -l info -Q foo,bar,baz
+ $ celery -A proj worker -l INFO -Q foo,bar,baz
If the queue name is defined in :setting:`task_queues` it will use that
configuration, but if it's not defined in the list of queues Celery will
diff --git a/examples/app/myapp.py b/examples/app/myapp.py
index 3490a3940bd..7ee8727095a 100644
--- a/examples/app/myapp.py
+++ b/examples/app/myapp.py
@@ -2,7 +2,7 @@
Usage::
- (window1)$ python myapp.py worker -l info
+ (window1)$ python myapp.py worker -l INFO
(window2)$ python
>>> from myapp import add
@@ -13,13 +13,13 @@
You can also specify the app to use with the `celery` command,
using the `-A` / `--app` option::
- $ celery -A myapp worker -l info
+ $ celery -A myapp worker -l INFO
With the `-A myproj` argument the program will search for an app
instance in the module ``myproj``. You can also specify an explicit
name using the fully qualified form::
- $ celery -A myapp:app worker -l info
+ $ celery -A myapp:app worker -l INFO
"""
diff --git a/examples/django/README.rst b/examples/django/README.rst
index 0334ef7df04..80d7a13cadd 100644
--- a/examples/django/README.rst
+++ b/examples/django/README.rst
@@ -46,7 +46,7 @@ Starting the worker
.. code-block:: console
- $ celery -A proj worker -l info
+ $ celery -A proj worker -l INFO
Running a task
===================
diff --git a/examples/eventlet/README.rst b/examples/eventlet/README.rst
index 672ff6f1461..84a1856f314 100644
--- a/examples/eventlet/README.rst
+++ b/examples/eventlet/README.rst
@@ -18,7 +18,7 @@ Before you run any of the example tasks you need to start
the worker::
$ cd examples/eventlet
- $ celery worker -l info --concurrency=500 --pool=eventlet
+ $ celery worker -l INFO --concurrency=500 --pool=eventlet
As usual you need to have RabbitMQ running, see the Celery getting started
guide if you haven't installed it yet.
diff --git a/examples/periodic-tasks/myapp.py b/examples/periodic-tasks/myapp.py
index 166b9234146..b2e4f0b8045 100644
--- a/examples/periodic-tasks/myapp.py
+++ b/examples/periodic-tasks/myapp.py
@@ -3,10 +3,10 @@
Usage::
# The worker service reacts to messages by executing tasks.
- (window1)$ python myapp.py worker -l info
+ (window1)$ python myapp.py worker -l INFO
# The beat service sends messages at scheduled intervals.
- (window2)$ python myapp.py beat -l info
+ (window2)$ python myapp.py beat -l INFO
# XXX To diagnose problems use -l debug:
(window2)$ python myapp.py beat -l debug
@@ -18,13 +18,13 @@
You can also specify the app to use with the `celery` command,
using the `-A` / `--app` option::
- $ celery -A myapp worker -l info
+ $ celery -A myapp worker -l INFO
With the `-A myproj` argument the program will search for an app
instance in the module ``myproj``. You can also specify an explicit
name using the fully qualified form::
- $ celery -A myapp:app worker -l info
+ $ celery -A myapp:app worker -l INFO
"""
diff --git a/examples/security/mysecureapp.py b/examples/security/mysecureapp.py
index 9578fa62272..21061a890da 100644
--- a/examples/security/mysecureapp.py
+++ b/examples/security/mysecureapp.py
@@ -14,7 +14,7 @@
cd examples/security
- (window1)$ python mysecureapp.py worker -l info
+ (window1)$ python mysecureapp.py worker -l INFO
(window2)$ cd examples/security
(window2)$ python
From 06dfe27d4542860c01c870a41fc5fbb80e5c4e20 Mon Sep 17 00:00:00 2001
From: ZubAnt
Date: Wed, 7 Oct 2020 20:33:59 +0300
Subject: [PATCH 023/415] return list instead set in CommaSeparatedList
_broadcast method of kombu Mailbox. does not support set
https://github.com/celery/kombu/blob/7b2578b19ba4b1989b722f6f6e7efee2a1a4d86a/kombu/pidbox.py#L319
---
celery/bin/base.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/celery/bin/base.py b/celery/bin/base.py
index fbb56d84dbb..52f94382c65 100644
--- a/celery/bin/base.py
+++ b/celery/bin/base.py
@@ -168,7 +168,7 @@ class CommaSeparatedList(ParamType):
name = "comma separated list"
def convert(self, value, param, ctx):
- return set(text.str_to_list(value))
+ return text.str_to_list(value)
class Json(ParamType):
From 735f1679047a1358254252edc5cbf2624c86aadc Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 13 Oct 2020 14:48:21 +0300
Subject: [PATCH 024/415] Rewrite detaching logic (#6401)
* Rewrite detaching logic.
* Ignore empty arguments.
* Ensure the SystemD services are up to date.
---
celery/apps/multi.py | 2 +-
celery/bin/worker.py | 53 +++++++++-------------
docs/userguide/daemonizing.rst | 75 ++++++++++++++++----------------
extra/systemd/celery.service | 10 ++---
extra/systemd/celerybeat.service | 6 +--
5 files changed, 68 insertions(+), 78 deletions(-)
diff --git a/celery/apps/multi.py b/celery/apps/multi.py
index a1458e3bd63..448c7cd6fbd 100644
--- a/celery/apps/multi.py
+++ b/celery/apps/multi.py
@@ -78,7 +78,7 @@ def __init__(self, args):
self.namespaces = defaultdict(lambda: OrderedDict())
def parse(self):
- rargs = list(self.args)
+ rargs = [arg for arg in self.args if arg]
pos = 0
while pos < len(rargs):
arg = rargs[pos]
diff --git a/celery/bin/worker.py b/celery/bin/worker.py
index bf58dbea647..0472fde4c4b 100644
--- a/celery/bin/worker.py
+++ b/celery/bin/worker.py
@@ -10,7 +10,8 @@
from celery import concurrency
from celery.bin.base import (COMMA_SEPARATED_LIST, LOG_LEVEL,
CeleryDaemonCommand, CeleryOption)
-from celery.platforms import EX_FAILURE, detached, maybe_drop_privileges
+from celery.platforms import (EX_FAILURE, EX_OK, detached,
+ maybe_drop_privileges)
from celery.utils.log import get_logger
from celery.utils.nodenames import default_nodename, host_format, node_format
@@ -99,6 +100,7 @@ def detach(path, argv, logfile=None, pidfile=None, uid=None,
if executable is not None:
path = executable
os.execv(path, [path] + argv)
+ return EX_OK
except Exception: # pylint: disable=broad-except
if app is None:
from celery import current_app
@@ -107,7 +109,7 @@ def detach(path, argv, logfile=None, pidfile=None, uid=None,
'ERROR', logfile, hostname=hostname)
logger.critical("Can't exec %r", ' '.join([path] + argv),
exc_info=True)
- return EX_FAILURE
+ return EX_FAILURE
@click.command(cls=CeleryDaemonCommand,
@@ -290,36 +292,23 @@ def worker(ctx, hostname=None, pool_cls=None, app=None, uid=None, gid=None,
"Unable to parse extra configuration from command line.\n"
f"Reason: {e}", ctx=ctx)
if kwargs.get('detach', False):
- params = ctx.params.copy()
- params.pop('detach')
- params.pop('logfile')
- params.pop('pidfile')
- params.pop('uid')
- params.pop('gid')
- umask = params.pop('umask')
- workdir = ctx.obj.workdir
- params.pop('hostname')
- executable = params.pop('executable')
- argv = ['-m', 'celery', 'worker']
- for arg, value in params.items():
- arg = arg.replace("_", "-")
- if isinstance(value, bool) and value:
- argv.append(f'--{arg}')
- else:
- if value is not None:
- argv.append(f'--{arg}')
- argv.append(str(value))
- return detach(sys.executable,
- argv,
- logfile=logfile,
- pidfile=pidfile,
- uid=uid, gid=gid,
- umask=umask,
- workdir=workdir,
- app=app,
- executable=executable,
- hostname=hostname)
- return
+ argv = ['-m', 'celery'] + sys.argv[1:]
+ if '--detach' in argv:
+ argv.remove('--detach')
+ if '-D' in argv:
+ argv.remove('-D')
+
+ return detach(sys.executable,
+ argv,
+ logfile=logfile,
+ pidfile=pidfile,
+ uid=uid, gid=gid,
+ umask=kwargs.get('umask', None),
+ workdir=kwargs.get('workdir', None),
+ app=app,
+ executable=kwargs.get('executable', None),
+ hostname=hostname)
+
maybe_drop_privileges(uid=uid, gid=gid)
worker = app.Worker(
hostname=hostname, pool_cls=pool_cls, loglevel=loglevel,
diff --git a/docs/userguide/daemonizing.rst b/docs/userguide/daemonizing.rst
index 07e39009c97..ae804f6c32e 100644
--- a/docs/userguide/daemonizing.rst
+++ b/docs/userguide/daemonizing.rst
@@ -389,27 +389,28 @@ This is an example systemd file:
.. code-block:: bash
- [Unit]
- Description=Celery Service
- After=network.target
-
- [Service]
- Type=forking
- User=celery
- Group=celery
- EnvironmentFile=/etc/conf.d/celery
- WorkingDirectory=/opt/celery
- ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} multi start ${CELERYD_NODES} \
- --pidfile=${CELERYD_PID_FILE} \
- --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS}'
- ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} \
- --pidfile=${CELERYD_PID_FILE}'
- ExecReload=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} multi restart ${CELERYD_NODES} \
- --pidfile=${CELERYD_PID_FILE} \
- --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS}'
-
- [Install]
- WantedBy=multi-user.target
+ [Unit]
+ Description=Celery Service
+ After=network.target
+
+ [Service]
+ Type=forking
+ User=celery
+ Group=celery
+ EnvironmentFile=/etc/conf.d/celery
+ WorkingDirectory=/opt/celery
+ ExecStart=/bin/sh -c '${CELERY_BIN} -A $CELERY_APP multi start $CELERYD_NODES \
+ --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
+ --loglevel="${CELERYD_LOG_LEVEL}" $CELERYD_OPTS'
+ ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait $CELERYD_NODES \
+ --pidfile=${CELERYD_PID_FILE} --loglevel="${CELERYD_LOG_LEVEL}"'
+ ExecReload=/bin/sh -c '${CELERY_BIN} -A $CELERY_APP multi restart $CELERYD_NODES \
+ --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
+ --loglevel="${CELERYD_LOG_LEVEL}" $CELERYD_OPTS'
+ Restart=always
+
+ [Install]
+ WantedBy=multi-user.target
Once you've put that file in :file:`/etc/systemd/system`, you should run
:command:`systemctl daemon-reload` in order that Systemd acknowledges that file.
@@ -482,22 +483,22 @@ This is an example systemd file for Celery Beat:
.. code-block:: bash
- [Unit]
- Description=Celery Beat Service
- After=network.target
-
- [Service]
- Type=simple
- User=celery
- Group=celery
- EnvironmentFile=/etc/conf.d/celery
- WorkingDirectory=/opt/celery
- ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} beat \
- --pidfile=${CELERYBEAT_PID_FILE} \
- --logfile=${CELERYBEAT_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
-
- [Install]
- WantedBy=multi-user.target
+ [Unit]
+ Description=Celery Beat Service
+ After=network.target
+
+ [Service]
+ Type=simple
+ User=celery
+ Group=celery
+ EnvironmentFile=/etc/conf.d/celery
+ WorkingDirectory=/opt/celery
+ ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} beat \
+ --pidfile=${CELERYBEAT_PID_FILE} \
+ --logfile=${CELERYBEAT_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
+
+ [Install]
+ WantedBy=multi-user.target
Running the worker with superuser privileges (root)
diff --git a/extra/systemd/celery.service b/extra/systemd/celery.service
index b1d6d03b723..2510fb83cb0 100644
--- a/extra/systemd/celery.service
+++ b/extra/systemd/celery.service
@@ -8,13 +8,13 @@ User=celery
Group=celery
EnvironmentFile=-/etc/conf.d/celery
WorkingDirectory=/opt/celery
-ExecStart=/bin/sh -c '${CELERY_BIN} multi start $CELERYD_NODES \
- -A $CELERY_APP --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
+ExecStart=/bin/sh -c '${CELERY_BIN} -A $CELERY_APP multi start $CELERYD_NODES \
+ --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
--loglevel="${CELERYD_LOG_LEVEL}" $CELERYD_OPTS'
ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait $CELERYD_NODES \
- --pidfile=${CELERYD_PID_FILE}'
-ExecReload=/bin/sh -c '${CELERY_BIN} multi restart $CELERYD_NODES \
- -A $CELERY_APP --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
+ --pidfile=${CELERYD_PID_FILE} --loglevel="${CELERYD_LOG_LEVEL}"'
+ExecReload=/bin/sh -c '${CELERY_BIN} -A $CELERY_APP multi restart $CELERYD_NODES \
+ --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
--loglevel="${CELERYD_LOG_LEVEL}" $CELERYD_OPTS'
Restart=always
diff --git a/extra/systemd/celerybeat.service b/extra/systemd/celerybeat.service
index c8879612d19..8cb2ad3687e 100644
--- a/extra/systemd/celerybeat.service
+++ b/extra/systemd/celerybeat.service
@@ -6,10 +6,10 @@ After=network.target
Type=simple
User=celery
Group=celery
-EnvironmentFile=-/etc/conf.d/celery
+EnvironmentFile=/etc/conf.d/celery
WorkingDirectory=/opt/celery
-ExecStart=/bin/sh -c '${CELERY_BIN} beat \
- -A ${CELERY_APP} --pidfile=${CELERYBEAT_PID_FILE} \
+ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} beat \
+ --pidfile=${CELERYBEAT_PID_FILE} \
--logfile=${CELERYBEAT_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
[Install]
From 9367d3615f99d6ec623dd235e7ec5387fe9926eb Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Tue, 13 Oct 2020 16:31:29 +1100
Subject: [PATCH 025/415] fix: Pass back real result for single task chains
When chains are delayed, they are first frozen as part of preparation
which causes the sub-tasks to also be frozen. Afterward, the final (0th
since we reverse the tasks/result order when freezing) result object
from the freezing process would be passed back to the caller. This
caused problems in signaling completion of groups contained in chains
because the group relies on a promise which is fulfilled by a barrier
linked to each of its applied subtasks. By constructing two
`GroupResult` objects (one during freezing, one when the chain sub-tasks
are applied), this resulted in there being two promises; only one of
which would actually be fulfilled by the group subtasks.
This change ensures that in the special case where a chain has a single
task, we pass back the result object constructed when the task was
actually applied. When that single child is a group which does not get
unrolled (ie. contains more than one child itself), this ensures that we
pass back a `GroupResult` object which will actually be fulfilled. The
caller can then await the result confidently!
---
celery/canvas.py | 16 +++++++++----
t/integration/test_canvas.py | 18 +++++++++++++++
t/unit/tasks/test_canvas.py | 44 +++++++++++++++++++++++++++++++++++-
3 files changed, 73 insertions(+), 5 deletions(-)
diff --git a/celery/canvas.py b/celery/canvas.py
index 7871f7b395d..866c1c888b2 100644
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -652,19 +652,27 @@ def run(self, args=None, kwargs=None, group_id=None, chord=None,
args = (tuple(args) + tuple(self.args)
if args and not self.immutable else self.args)
- tasks, results = self.prepare_steps(
+ tasks, results_from_prepare = self.prepare_steps(
args, kwargs, self.tasks, root_id, parent_id, link_error, app,
task_id, group_id, chord,
)
- if results:
+ if results_from_prepare:
if link:
tasks[0].extend_list_option('link', link)
first_task = tasks.pop()
options = _prepare_chain_from_options(options, tasks, use_link)
- first_task.apply_async(**options)
- return results[0]
+ result_from_apply = first_task.apply_async(**options)
+ # If we only have a single task, it may be important that we pass
+ # the real result object rather than the one obtained via freezing.
+ # e.g. For `GroupResult`s, we need to pass back the result object
+ # which will actually have its promise fulfilled by the subtasks,
+ # something that will never occur for the frozen result.
+ if not tasks:
+ return result_from_apply
+ else:
+ return results_from_prepare[0]
def freeze(self, _id=None, group_id=None, chord=None,
root_id=None, parent_id=None, group_index=None):
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index f5d19184a34..6b1f316b03e 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -413,6 +413,16 @@ def test_chain_of_a_chord_and_three_tasks_and_a_group(self, manager):
res = c()
assert res.get(timeout=TIMEOUT) == [8, 8]
+ def test_nested_chain_group_lone(self, manager):
+ """
+ Test that a lone group in a chain completes.
+ """
+ sig = chain(
+ group(identity.s(42), identity.s(42)), # [42, 42]
+ )
+ res = sig.delay()
+ assert res.get(timeout=TIMEOUT) == [42, 42]
+
class test_result_set:
@@ -504,6 +514,14 @@ def test_large_group(self, manager):
assert res.get(timeout=TIMEOUT) == list(range(1000))
+ def test_group_lone(self, manager):
+ """
+ Test that a simple group completes.
+ """
+ sig = group(identity.s(42), identity.s(42)) # [42, 42]
+ res = sig.delay()
+ assert res.get(timeout=TIMEOUT) == [42, 42]
+
def assert_ids(r, expected_value, expected_root_id, expected_parent_id):
root_id, parent_id, value = r.get(timeout=TIMEOUT)
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
index 53f98615e8e..c15dec83d60 100644
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -1,5 +1,5 @@
import json
-from unittest.mock import MagicMock, Mock
+from unittest.mock import MagicMock, Mock, patch
import pytest
@@ -535,6 +535,48 @@ def test_append_to_empty_chain(self):
assert x.apply().get() == 3
+ @pytest.mark.usefixtures('depends_on_current_app')
+ def test_chain_single_child_result(self):
+ child_sig = self.add.si(1, 1)
+ chain_sig = chain(child_sig)
+ assert chain_sig.tasks[0] is child_sig
+
+ with patch.object(
+ # We want to get back the result of actually applying the task
+ child_sig, "apply_async",
+ ) as mock_apply, patch.object(
+ # The child signature may be clone by `chain.prepare_steps()`
+ child_sig, "clone", return_value=child_sig,
+ ):
+ res = chain_sig()
+ # `_prepare_chain_from_options()` sets this `chain` kwarg with the
+ # subsequent tasks which would be run - nothing in this case
+ mock_apply.assert_called_once_with(chain=[])
+ assert res is mock_apply.return_value
+
+ @pytest.mark.usefixtures('depends_on_current_app')
+ def test_chain_single_child_group_result(self):
+ child_sig = self.add.si(1, 1)
+ # The group will `clone()` the child during instantiation so mock it
+ with patch.object(child_sig, "clone", return_value=child_sig):
+ group_sig = group(child_sig)
+ # Now we can construct the chain signature which is actually under test
+ chain_sig = chain(group_sig)
+ assert chain_sig.tasks[0].tasks[0] is child_sig
+
+ with patch.object(
+ # We want to get back the result of actually applying the task
+ child_sig, "apply_async",
+ ) as mock_apply, patch.object(
+ # The child signature may be clone by `chain.prepare_steps()`
+ child_sig, "clone", return_value=child_sig,
+ ):
+ res = chain_sig()
+ # `_prepare_chain_from_options()` sets this `chain` kwarg with the
+ # subsequent tasks which would be run - nothing in this case
+ mock_apply.assert_called_once_with(chain=[])
+ assert res is mock_apply.return_value
+
class test_group(CanvasCase):
From f1dbf3f05fb047c4d57c61b4ccaa0dcedd16e193 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Fri, 18 Sep 2020 14:23:40 +1000
Subject: [PATCH 026/415] fix: Retain `group_id` when tasks get re-frozen
When a group task which is part of a chain was to be delayed by
`trace_task()`, it would be reconstructed from the serialized request.
Normally, this sets the `group_id` of encapsulated tasks to the ID of
the group being instantiated. However, in the specific situation of a
group that is the last task in a chain which contributes to the
completion of a chord, it is essential that the group ID of the top-most
group is used instead. This top-most group ID is used by the redis
backend to track the completions of "final elements" of a chord in the
`on_chord_part_return()` implementation. By overwriting the group ID
which was already set in the `options` dictionaries of the child tasks
being deserialized, the chord accounting done by the redis backend would
be made inaccurate and chords would never complete.
This change alters how options are overridden for signatures to ensure
that if a `group_id` has already been set, it cannot be overridden.
Since group ID should be generally opaque to users, this should not be
disruptive.
---
celery/canvas.py | 23 +++++++++++++++++------
t/unit/tasks/test_canvas.py | 25 ++++++++++++++++++++++++-
2 files changed, 41 insertions(+), 7 deletions(-)
diff --git a/celery/canvas.py b/celery/canvas.py
index 866c1c888b2..f767de1ce0a 100644
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -122,6 +122,9 @@ class Signature(dict):
TYPES = {}
_app = _type = None
+ # The following fields must not be changed during freezing/merging because
+ # to do so would disrupt completion of parent tasks
+ _IMMUTABLE_OPTIONS = {"group_id"}
@classmethod
def register_type(cls, name=None):
@@ -224,14 +227,22 @@ def apply_async(self, args=None, kwargs=None, route_name=None, **options):
def _merge(self, args=None, kwargs=None, options=None, force=False):
args = args if args else ()
kwargs = kwargs if kwargs else {}
- options = options if options else {}
+ if options is not None:
+ # We build a new options dictionary where values in `options`
+ # override values in `self.options` except for keys which are
+ # noted as being immutable (unrelated to signature immutability)
+ # implying that allowing their value to change would stall tasks
+ new_options = dict(self.options, **{
+ k: v for k, v in options.items()
+ if k not in self._IMMUTABLE_OPTIONS or k not in self.options
+ })
+ else:
+ new_options = self.options
if self.immutable and not force:
- return (self.args, self.kwargs,
- dict(self.options,
- **options) if options else self.options)
+ return (self.args, self.kwargs, new_options)
return (tuple(args) + tuple(self.args) if args else self.args,
dict(self.kwargs, **kwargs) if kwargs else self.kwargs,
- dict(self.options, **options) if options else self.options)
+ new_options)
def clone(self, args=None, kwargs=None, **opts):
"""Create a copy of this signature.
@@ -286,7 +297,7 @@ def freeze(self, _id=None, group_id=None, chord=None,
opts['parent_id'] = parent_id
if 'reply_to' not in opts:
opts['reply_to'] = self.app.oid
- if group_id:
+ if group_id and "group_id" not in opts:
opts['group_id'] = group_id
if chord:
opts['chord'] = chord
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
index c15dec83d60..b90321572f3 100644
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -1,5 +1,5 @@
import json
-from unittest.mock import MagicMock, Mock, patch
+from unittest.mock import MagicMock, Mock, patch, sentinel
import pytest
@@ -154,6 +154,29 @@ def test_merge_immutable(self):
assert kwargs == {'foo': 1}
assert options == {'task_id': 3}
+ def test_merge_options__none(self):
+ sig = self.add.si()
+ _, _, new_options = sig._merge()
+ assert new_options is sig.options
+ _, _, new_options = sig._merge(options=None)
+ assert new_options is sig.options
+
+ @pytest.mark.parametrize("immutable_sig", (True, False))
+ def test_merge_options__group_id(self, immutable_sig):
+ # This is to avoid testing the behaviour in `test_set_immutable()`
+ if immutable_sig:
+ sig = self.add.si()
+ else:
+ sig = self.add.s()
+ # If the signature has no group ID, it can be set
+ assert not sig.options
+ _, _, new_options = sig._merge(options={"group_id": sentinel.gid})
+ assert new_options == {"group_id": sentinel.gid}
+ # But if one is already set, the new one is silently ignored
+ sig.set(group_id=sentinel.old_gid)
+ _, _, new_options = sig._merge(options={"group_id": sentinel.new_gid})
+ assert new_options == {"group_id": sentinel.old_gid}
+
def test_set_immutable(self):
x = self.add.s(2, 2)
assert not x.immutable
From a7af4b28c4151fde2de70802a0ba2b10efc836d8 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Fri, 18 Sep 2020 14:38:46 +1000
Subject: [PATCH 027/415] fix: Count chord "final elements" correctly
This change amends the implementation of `chord.__length_hint__()` to
ensure that all child task types are correctly counted. Specifically:
* all sub-tasks of a group are counted recursively
* the final task of a chain is counted recursively
* the body of a chord is counted recursively
* all other simple signatures count as a single "final element"
There is also a deserialisation step if a `dict` is seen while counting
the final elements in a chord, however this should become less important
with the merge of #6342 which ensures that tasks are recursively
deserialized by `.from_dict()`.
---
celery/canvas.py | 35 ++++---
t/integration/test_canvas.py | 22 +++++
t/unit/tasks/test_canvas.py | 181 +++++++++++++++++++++++++++++++++--
3 files changed, 218 insertions(+), 20 deletions(-)
diff --git a/celery/canvas.py b/celery/canvas.py
index f767de1ce0a..2150d0e872d 100644
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -1383,21 +1383,30 @@ def apply(self, args=None, kwargs=None,
args=(tasks.apply(args, kwargs).get(propagate=propagate),),
)
- def _traverse_tasks(self, tasks, value=None):
- stack = deque(tasks)
- while stack:
- task = stack.popleft()
- if isinstance(task, group):
- stack.extend(task.tasks)
- elif isinstance(task, _chain) and isinstance(task.tasks[-1], group):
- stack.extend(task.tasks[-1].tasks)
- else:
- yield task if value is None else value
+ @classmethod
+ def __descend(cls, sig_obj):
+ # Sometimes serialized signatures might make their way here
+ if not isinstance(sig_obj, Signature) and isinstance(sig_obj, dict):
+ sig_obj = Signature.from_dict(sig_obj)
+ if isinstance(sig_obj, group):
+ # Each task in a group counts toward this chord
+ subtasks = getattr(sig_obj.tasks, "tasks", sig_obj.tasks)
+ return sum(cls.__descend(task) for task in subtasks)
+ elif isinstance(sig_obj, _chain):
+ # The last element in a chain counts toward this chord
+ return cls.__descend(sig_obj.tasks[-1])
+ elif isinstance(sig_obj, chord):
+ # The child chord's body counts toward this chord
+ return cls.__descend(sig_obj.body)
+ elif isinstance(sig_obj, Signature):
+ # Each simple signature counts as 1 completion for this chord
+ return 1
+ # Any other types are assumed to be iterables of simple signatures
+ return len(sig_obj)
def __length_hint__(self):
- tasks = (self.tasks.tasks if isinstance(self.tasks, group)
- else self.tasks)
- return sum(self._traverse_tasks(tasks, 1))
+ tasks = getattr(self.tasks, "tasks", self.tasks)
+ return sum(self.__descend(task) for task in tasks)
def run(self, header, body, partial_args, app=None, interval=None,
countdown=1, max_retries=None, eager=False,
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 6b1f316b03e..c7e00b196a7 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -1009,3 +1009,25 @@ def test_priority_chain(self, manager):
c = return_priority.signature(priority=3) | return_priority.signature(
priority=5)
assert c().get(timeout=TIMEOUT) == "Priority: 5"
+
+ def test_nested_chord_group_chain_group_tail(self, manager):
+ """
+ Sanity check that a deeply nested group is completed as expected.
+
+ Groups at the end of chains nested in chords have had issues and this
+ simple test sanity check that such a tsk structure can be completed.
+ """
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ sig = chord(group(chain(
+ identity.s(42), # -> 42
+ group(
+ identity.s(), # -> 42
+ identity.s(), # -> 42
+ ), # [42, 42]
+ )), identity.s()) # [[42, 42]]
+ res = sig.delay()
+ assert res.get(timeout=TIMEOUT) == [[42, 42]]
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
index b90321572f3..f51efab9389 100644
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -1,5 +1,5 @@
import json
-from unittest.mock import MagicMock, Mock, patch, sentinel
+from unittest.mock import MagicMock, Mock, call, patch, sentinel
import pytest
@@ -808,12 +808,179 @@ def test_app_fallback_to_current(self):
x = chord([t1], body=t1)
assert x.app is current_app
- def test_chord_size_with_groups(self):
- x = chord([
- self.add.s(2, 2) | group([self.add.si(2, 2), self.add.si(2, 2)]),
- self.add.s(2, 2) | group([self.add.si(2, 2), self.add.si(2, 2)]),
- ], body=self.add.si(2, 2))
- assert x.__length_hint__() == 4
+ def test_chord_size_simple(self):
+ sig = chord(self.add.s())
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_with_body(self):
+ sig = chord(self.add.s(), self.add.s())
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_explicit_group_single(self):
+ sig = chord(group(self.add.s()))
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_explicit_group_many(self):
+ sig = chord(group([self.add.s()] * 42))
+ assert sig.__length_hint__() == 42
+
+ def test_chord_size_implicit_group_single(self):
+ sig = chord([self.add.s()])
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_implicit_group_many(self):
+ sig = chord([self.add.s()] * 42)
+ assert sig.__length_hint__() == 42
+
+ def test_chord_size_chain_single(self):
+ sig = chord(chain(self.add.s()))
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_chain_many(self):
+ # Chains get flattened into the encapsulating chord so even though the
+ # chain would only count for 1, the tasks we pulled into the chord's
+ # header and are counted as a bunch of simple signature objects
+ sig = chord(chain([self.add.s()] * 42))
+ assert sig.__length_hint__() == 42
+
+ def test_chord_size_nested_chain_chain_single(self):
+ sig = chord(chain(chain(self.add.s())))
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_nested_chain_chain_many(self):
+ # The outer chain will be pulled up into the chord but the lower one
+ # remains and will only count as a single final element
+ sig = chord(chain(chain([self.add.s()] * 42)))
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_implicit_chain_single(self):
+ sig = chord([self.add.s()])
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_implicit_chain_many(self):
+ # This isn't a chain object so the `tasks` attribute can't be lifted
+ # into the chord - this isn't actually valid and would blow up we tried
+ # to run it but it sanity checks our recursion
+ sig = chord([[self.add.s()] * 42])
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_nested_implicit_chain_chain_single(self):
+ sig = chord([chain(self.add.s())])
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_nested_implicit_chain_chain_many(self):
+ sig = chord([chain([self.add.s()] * 42)])
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_nested_chord_body_simple(self):
+ sig = chord(chord(tuple(), self.add.s()))
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_nested_chord_body_implicit_group_single(self):
+ sig = chord(chord(tuple(), [self.add.s()]))
+ assert sig.__length_hint__() == 1
+
+ def test_chord_size_nested_chord_body_implicit_group_many(self):
+ sig = chord(chord(tuple(), [self.add.s()] * 42))
+ assert sig.__length_hint__() == 42
+
+ # Nested groups in a chain only affect the chord size if they are the last
+ # element in the chain - in that case each group element is counted
+ def test_chord_size_nested_group_chain_group_head_single(self):
+ x = chord(
+ group(
+ [group(self.add.s()) | self.add.s()] * 42
+ ),
+ body=self.add.s()
+ )
+ assert x.__length_hint__() == 42
+
+ def test_chord_size_nested_group_chain_group_head_many(self):
+ x = chord(
+ group(
+ [group([self.add.s()] * 4) | self.add.s()] * 2
+ ),
+ body=self.add.s()
+ )
+ assert x.__length_hint__() == 2
+
+ def test_chord_size_nested_group_chain_group_mid_single(self):
+ x = chord(
+ group(
+ [self.add.s() | group(self.add.s()) | self.add.s()] * 42
+ ),
+ body=self.add.s()
+ )
+ assert x.__length_hint__() == 42
+
+ def test_chord_size_nested_group_chain_group_mid_many(self):
+ x = chord(
+ group(
+ [self.add.s() | group([self.add.s()] * 4) | self.add.s()] * 2
+ ),
+ body=self.add.s()
+ )
+ assert x.__length_hint__() == 2
+
+ def test_chord_size_nested_group_chain_group_tail_single(self):
+ x = chord(
+ group(
+ [self.add.s() | group(self.add.s())] * 42
+ ),
+ body=self.add.s()
+ )
+ assert x.__length_hint__() == 42
+
+ def test_chord_size_nested_group_chain_group_tail_many(self):
+ x = chord(
+ group(
+ [self.add.s() | group([self.add.s()] * 4)] * 2
+ ),
+ body=self.add.s()
+ )
+ assert x.__length_hint__() == 4 * 2
+
+ def test_chord_size_nested_implicit_group_chain_group_tail_single(self):
+ x = chord(
+ [self.add.s() | group(self.add.s())] * 42,
+ body=self.add.s()
+ )
+ assert x.__length_hint__() == 42
+
+ def test_chord_size_nested_implicit_group_chain_group_tail_many(self):
+ x = chord(
+ [self.add.s() | group([self.add.s()] * 4)] * 2,
+ body=self.add.s()
+ )
+ assert x.__length_hint__() == 4 * 2
+
+ def test_chord_size_deserialized_element_single(self):
+ child_sig = self.add.s()
+ deserialized_child_sig = json.loads(json.dumps(child_sig))
+ # We have to break in to be sure that a child remains as a `dict` so we
+ # can confirm that the length hint will instantiate a `Signature`
+ # object and then descend as expected
+ chord_sig = chord(tuple())
+ chord_sig.tasks = [deserialized_child_sig]
+ with patch(
+ "celery.canvas.Signature.from_dict", return_value=child_sig
+ ) as mock_from_dict:
+ assert chord_sig. __length_hint__() == 1
+ mock_from_dict.assert_called_once_with(deserialized_child_sig)
+
+ def test_chord_size_deserialized_element_many(self):
+ child_sig = self.add.s()
+ deserialized_child_sig = json.loads(json.dumps(child_sig))
+ # We have to break in to be sure that a child remains as a `dict` so we
+ # can confirm that the length hint will instantiate a `Signature`
+ # object and then descend as expected
+ chord_sig = chord(tuple())
+ chord_sig.tasks = [deserialized_child_sig] * 42
+ with patch(
+ "celery.canvas.Signature.from_dict", return_value=child_sig
+ ) as mock_from_dict:
+ assert chord_sig. __length_hint__() == 42
+ mock_from_dict.assert_has_calls([call(deserialized_child_sig)] * 42)
def test_set_immutable(self):
x = chord([Mock(name='t1'), Mock(name='t2')], app=self.app)
From 53e032d28c504a0beda4a497c22f41bce5090594 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Tue, 13 Oct 2020 16:37:53 +1100
Subject: [PATCH 028/415] test: Add more integration tests for groups
These tests are intended to show that group unrolling should be
respected in various ways by all backends. They should make it more
clear what behaviour we should be expecting from nested canvas
components and ensure that all the implementations (mostly relevant to
chords and `on_chord_part_return()` code) behave sensibly.
---
t/integration/test_canvas.py | 73 +++++++++++++++++++++++++++++++++---
1 file changed, 67 insertions(+), 6 deletions(-)
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index c7e00b196a7..96b112af8f9 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -423,6 +423,34 @@ def test_nested_chain_group_lone(self, manager):
res = sig.delay()
assert res.get(timeout=TIMEOUT) == [42, 42]
+ def test_nested_chain_group_mid(self, manager):
+ """
+ Test that a mid-point group in a chain completes.
+ """
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ sig = chain(
+ identity.s(42), # 42
+ group(identity.s(), identity.s()), # [42, 42]
+ identity.s(), # [42, 42]
+ )
+ res = sig.delay()
+ assert res.get(timeout=TIMEOUT) == [42, 42]
+
+ def test_nested_chain_group_last(self, manager):
+ """
+ Test that a final group in a chain with preceding tasks completes.
+ """
+ sig = chain(
+ identity.s(42), # 42
+ group(identity.s(), identity.s()), # [42, 42]
+ )
+ res = sig.delay()
+ assert res.get(timeout=TIMEOUT) == [42, 42]
+
class test_result_set:
@@ -522,6 +550,16 @@ def test_group_lone(self, manager):
res = sig.delay()
assert res.get(timeout=TIMEOUT) == [42, 42]
+ def test_nested_group_group(self, manager):
+ """
+ Confirm that groups nested inside groups get unrolled.
+ """
+ sig = group(
+ group(identity.s(42), identity.s(42)), # [42, 42]
+ ) # [42, 42] due to unrolling
+ res = sig.delay()
+ assert res.get(timeout=TIMEOUT) == [42, 42]
+
def assert_ids(r, expected_value, expected_root_id, expected_parent_id):
root_id, parent_id, value = r.get(timeout=TIMEOUT)
@@ -1010,6 +1048,24 @@ def test_priority_chain(self, manager):
priority=5)
assert c().get(timeout=TIMEOUT) == "Priority: 5"
+ def test_nested_chord_group(self, manager):
+ """
+ Confirm that groups nested inside chords get unrolled.
+ """
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ sig = chord(
+ (
+ group(identity.s(42), identity.s(42)), # [42, 42]
+ ),
+ identity.s() # [42, 42]
+ )
+ res = sig.delay()
+ assert res.get(timeout=TIMEOUT) == [42, 42]
+
def test_nested_chord_group_chain_group_tail(self, manager):
"""
Sanity check that a deeply nested group is completed as expected.
@@ -1022,12 +1078,17 @@ def test_nested_chord_group_chain_group_tail(self, manager):
except NotImplementedError as e:
raise pytest.skip(e.args[0])
- sig = chord(group(chain(
- identity.s(42), # -> 42
+ sig = chord(
group(
- identity.s(), # -> 42
- identity.s(), # -> 42
- ), # [42, 42]
- )), identity.s()) # [[42, 42]]
+ chain(
+ identity.s(42), # 42
+ group(
+ identity.s(), # 42
+ identity.s(), # 42
+ ), # [42, 42]
+ ), # [42, 42]
+ ), # [[42, 42]] since the chain prevents unrolling
+ identity.s(), # [[42, 42]]
+ )
res = sig.delay()
assert res.get(timeout=TIMEOUT) == [[42, 42]]
From ea2a803d57524db1edf0ecf81164e3c61fc8935b Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Tue, 13 Oct 2020 16:44:13 +1100
Subject: [PATCH 029/415] test: Fix old markings for chord tests
---
t/integration/test_canvas.py | 16 ++++++----------
1 file changed, 6 insertions(+), 10 deletions(-)
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 96b112af8f9..690acef352c 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -1,4 +1,3 @@
-import os
from datetime import datetime, timedelta
from time import sleep
@@ -6,7 +5,7 @@
from celery import chain, chord, group, signature
from celery.backends.base import BaseKeyValueStoreBackend
-from celery.exceptions import ChordError, TimeoutError
+from celery.exceptions import TimeoutError
from celery.result import AsyncResult, GroupResult, ResultSet
from .conftest import get_active_redis_channels, get_redis_connection
@@ -691,10 +690,12 @@ def test_eager_chord_inside_task(self, manager):
chord_add.app.conf.task_always_eager = prev
- @flaky
def test_group_chain(self, manager):
- if not manager.app.conf.result_backend.startswith('redis'):
- raise pytest.skip('Requires redis result backend.')
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
c = (
add.s(2, 2) |
group(add.s(i) for i in range(4)) |
@@ -703,11 +704,6 @@ def test_group_chain(self, manager):
res = c()
assert res.get(timeout=TIMEOUT) == [12, 13, 14, 15]
- @flaky
- @pytest.mark.xfail(os.environ['TEST_BACKEND'] == 'cache+pylibmc://',
- reason="Not supported yet by the cache backend.",
- strict=True,
- raises=ChordError)
def test_nested_group_chain(self, manager):
try:
manager.app.backend.ensure_chords_allowed()
From 62f37133c8681584ae6f7b3499551b52ab369ea2 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Tue, 13 Oct 2020 16:23:44 +1100
Subject: [PATCH 030/415] fix: Make KV-store backends respect chord size
This avoids an issue where the `on_chord_part_return()` implementation
would check the the length of the result of a chain ending in a nested
group. This would manifest in behaviour where a worker would be blocked
waiting for for the result object it holds to complete since it would
attempt to `.join()` the result object. In situations with plenty of
workers, this wouldn't really cause any noticable issue apart from some
latency or unpredictable failures - but in concurrency constrained
situations like the integrations tests, it causes deadlocks.
We know from previous commits in this series that chord completion is
more complex than just waiting for a direct child, so we correct the
`size` value in `BaseKeyValueStoreBackend.on_chord_part_return()` to
respect the `chord_size` value from the request, falling back to the
length of the `deps` if that value is missing for some reason (this is
necessary to keep a number of the tests happy but it's not clear to me
if that will ever be the case in real life situations).
---
celery/backends/base.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/celery/backends/base.py b/celery/backends/base.py
index 28e5b2a4d6b..74fce23c3c4 100644
--- a/celery/backends/base.py
+++ b/celery/backends/base.py
@@ -919,7 +919,11 @@ def on_chord_part_return(self, request, state, result, **kwargs):
ChordError(f'GroupResult {gid} no longer exists'),
)
val = self.incr(key)
- size = len(deps)
+ # Set the chord size to the value defined in the request, or fall back
+ # to the number of dependencies we can see from the restored result
+ size = request.chord.get("chord_size")
+ if size is None:
+ size = len(deps)
if val > size: # pragma: no cover
logger.warning('Chord counter incremented too many times for %r',
gid)
From beddbeef16f76b72cbfe89e7fd1a1375f7305dfa Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Wed, 14 Oct 2020 10:39:19 +1100
Subject: [PATCH 031/415] fix: Retain chord header result structure in Redis
This change fixes the chord result flattening issue which manifested
when using the Redis backend due to its deliberate throwing away of
information about the header result structure. Rather than assuming that
all results which contribute to the finalisation of a chord should be
siblings, this change checks if any are complex (ie. `GroupResult`s) and
falls back to behaviour similar to that implemented in the
`KeyValueStoreBackend` which restores the original `GroupResult` object
and `join()`s it.
We retain the original behaviour which is billed as an optimisation in
f09b041. We could behave better in the complex header result case by not
bothering to stash the results of contributing tasks under the `.j` zset
since we won't be using them, but without checking for the presence of
the complex group result on every `on_chord_part_return()` call, we
can't be sure that we won't need those stashed results later on. This
would be an opportunity for optimisation in future if we were to use an
`EVAL` to only do the `zadd()` if the group result key doesn't exist.
However, avoiding the result encoding work in `on_chord_part_return()`
would be more complicated. For now, it's not worth the brainpower.
This change also slightly refactors the redis backend unit tests to make
it easier to build fixtures and hit both the complex and simple result
structure cases.
---
celery/backends/redis.py | 65 ++++++++----
t/integration/test_canvas.py | 13 ++-
t/unit/backends/test_redis.py | 193 ++++++++++++++++++++++++----------
3 files changed, 194 insertions(+), 77 deletions(-)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index 2c428823538..dd3677f569c 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -13,6 +13,7 @@
from celery._state import task_join_will_block
from celery.canvas import maybe_signature
from celery.exceptions import ChordError, ImproperlyConfigured
+from celery.result import GroupResult, allow_join_result
from celery.utils.functional import dictfilter
from celery.utils.log import get_logger
from celery.utils.time import humanize_seconds
@@ -401,12 +402,14 @@ def _unpack_chord_result(self, tup, decode,
return retval
def apply_chord(self, header_result, body, **kwargs):
- # Overrides this to avoid calling GroupResult.save
- # pylint: disable=method-hidden
- # Note that KeyValueStoreBackend.__init__ sets self.apply_chord
- # if the implements_incr attr is set. Redis backend doesn't set
- # this flag.
- pass
+ # If any of the child results of this chord are complex (ie. group
+ # results themselves), we need to save `header_result` to ensure that
+ # the expected structure is retained when we finish the chord and pass
+ # the results onward to the body in `on_chord_part_return()`. We don't
+ # do this is all cases to retain an optimisation in the common case
+ # where a chord header is comprised of simple result objects.
+ if any(isinstance(nr, GroupResult) for nr in header_result.results):
+ header_result.save(backend=self)
@cached_property
def _chord_zset(self):
@@ -449,20 +452,38 @@ def on_chord_part_return(self, request, state, result,
callback = maybe_signature(request.chord, app=app)
total = callback['chord_size'] + totaldiff
if readycount == total:
- decode, unpack = self.decode, self._unpack_chord_result
- with client.pipeline() as pipe:
- if self._chord_zset:
- pipeline = pipe.zrange(jkey, 0, -1)
- else:
- pipeline = pipe.lrange(jkey, 0, total)
- resl, = pipeline.execute()
- try:
- callback.delay([unpack(tup, decode) for tup in resl])
+ header_result = GroupResult.restore(gid)
+ if header_result is not None:
+ # If we manage to restore a `GroupResult`, then it must
+ # have been complex and saved by `apply_chord()` earlier.
+ #
+ # Before we can join the `GroupResult`, it needs to be
+ # manually marked as ready to avoid blocking
+ header_result.on_ready()
+ # We'll `join()` it to get the results and ensure they are
+ # structured as intended rather than the flattened version
+ # we'd construct without any other information.
+ join_func = (
+ header_result.join_native
+ if header_result.supports_native_join
+ else header_result.join
+ )
+ with allow_join_result():
+ resl = join_func(timeout=3.0, propagate=True)
+ else:
+ # Otherwise simply extract and decode the results we
+ # stashed along the way, which should be faster for large
+ # numbers of simple results in the chord header.
+ decode, unpack = self.decode, self._unpack_chord_result
with client.pipeline() as pipe:
- _, _ = pipe \
- .delete(jkey) \
- .delete(tkey) \
- .execute()
+ if self._chord_zset:
+ pipeline = pipe.zrange(jkey, 0, -1)
+ else:
+ pipeline = pipe.lrange(jkey, 0, total)
+ resl, = pipeline.execute()
+ resl = [unpack(tup, decode) for tup in resl]
+ try:
+ callback.delay(resl)
except Exception as exc: # pylint: disable=broad-except
logger.exception(
'Chord callback for %r raised: %r', request.group, exc)
@@ -470,6 +491,12 @@ def on_chord_part_return(self, request, state, result,
callback,
ChordError(f'Callback error: {exc!r}'),
)
+ finally:
+ with client.pipeline() as pipe:
+ _, _ = pipe \
+ .delete(jkey) \
+ .delete(tkey) \
+ .execute()
except ChordError as exc:
logger.exception('Chord %r raised: %r', request.group, exc)
return self.chord_error_from_stack(callback, exc)
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 690acef352c..256ecdbd9ee 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -1,3 +1,4 @@
+import re
from datetime import datetime, timedelta
from time import sleep
@@ -892,9 +893,15 @@ def test_chord_on_error(self, manager):
# So for clarity of our test, we instead do it here.
# Use the error callback's result to find the failed task.
- error_callback_result = AsyncResult(
- res.children[0].children[0].result[0])
- failed_task_id = error_callback_result.result.args[0].split()[3]
+ uuid_patt = re.compile(
+ r"[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}"
+ )
+ callback_chord_exc = AsyncResult(
+ res.children[0].children[0].result[0]
+ ).result
+ failed_task_id = uuid_patt.search(str(callback_chord_exc))
+ assert (failed_task_id is not None), "No task ID in %r" % callback_exc
+ failed_task_id = failed_task_id.group()
# Use new group_id result metadata to get group ID.
failed_task_result = AsyncResult(failed_task_id)
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index 2029edc3c29..f534077a4fd 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -1,3 +1,4 @@
+import itertools
import json
import random
import ssl
@@ -274,7 +275,7 @@ def test_drain_events_connection_error(self, parent_on_state_change, cancel_for)
assert consumer._pubsub._subscribed_to == {b'celery-task-meta-initial'}
-class test_RedisBackend:
+class basetest_RedisBackend:
def get_backend(self):
from celery.backends.redis import RedisBackend
@@ -287,11 +288,42 @@ def get_E_LOST(self):
from celery.backends.redis import E_LOST
return E_LOST
+ def create_task(self, i, group_id="group_id"):
+ tid = uuid()
+ task = Mock(name=f'task-{tid}')
+ task.name = 'foobarbaz'
+ self.app.tasks['foobarbaz'] = task
+ task.request.chord = signature(task)
+ task.request.id = tid
+ task.request.chord['chord_size'] = 10
+ task.request.group = group_id
+ task.request.group_index = i
+ return task
+
+ @contextmanager
+ def chord_context(self, size=1):
+ with patch('celery.backends.redis.maybe_signature') as ms:
+ request = Mock(name='request')
+ request.id = 'id1'
+ request.group = 'gid1'
+ request.group_index = None
+ tasks = [
+ self.create_task(i, group_id=request.group)
+ for i in range(size)
+ ]
+ callback = ms.return_value = Signature('add')
+ callback.id = 'id1'
+ callback['chord_size'] = size
+ callback.delay = Mock(name='callback.delay')
+ yield tasks, request, callback
+
def setup(self):
self.Backend = self.get_backend()
self.E_LOST = self.get_E_LOST()
self.b = self.Backend(app=self.app)
+
+class test_RedisBackend(basetest_RedisBackend):
@pytest.mark.usefixtures('depends_on_current_app')
def test_reduce(self):
pytest.importorskip('redis')
@@ -623,20 +655,36 @@ def test_set_no_expire(self):
self.b.expires = None
self.b._set_with_state('foo', 'bar', states.SUCCESS)
- def create_task(self, i):
+ def test_process_cleanup(self):
+ self.b.process_cleanup()
+
+ def test_get_set_forget(self):
tid = uuid()
- task = Mock(name=f'task-{tid}')
- task.name = 'foobarbaz'
- self.app.tasks['foobarbaz'] = task
- task.request.chord = signature(task)
- task.request.id = tid
- task.request.chord['chord_size'] = 10
- task.request.group = 'group_id'
- task.request.group_index = i
- return task
+ self.b.store_result(tid, 42, states.SUCCESS)
+ assert self.b.get_state(tid) == states.SUCCESS
+ assert self.b.get_result(tid) == 42
+ self.b.forget(tid)
+ assert self.b.get_state(tid) == states.PENDING
- @patch('celery.result.GroupResult.restore')
- def test_on_chord_part_return(self, restore):
+ def test_set_expires(self):
+ self.b = self.Backend(expires=512, app=self.app)
+ tid = uuid()
+ key = self.b.get_key_for_task(tid)
+ self.b.store_result(tid, 42, states.SUCCESS)
+ self.b.client.expire.assert_called_with(
+ key, 512,
+ )
+
+
+class test_RedisBackend_chords_simple(basetest_RedisBackend):
+ @pytest.fixture(scope="class", autouse=True)
+ def simple_header_result(self):
+ with patch(
+ "celery.result.GroupResult.restore", return_value=None,
+ ) as p:
+ yield p
+
+ def test_on_chord_part_return(self):
tasks = [self.create_task(i) for i in range(10)]
random.shuffle(tasks)
@@ -652,8 +700,7 @@ def test_on_chord_part_return(self, restore):
call(jkey, 86400), call(tkey, 86400),
])
- @patch('celery.result.GroupResult.restore')
- def test_on_chord_part_return__unordered(self, restore):
+ def test_on_chord_part_return__unordered(self):
self.app.conf.result_backend_transport_options = dict(
result_chord_ordered=False,
)
@@ -673,8 +720,7 @@ def test_on_chord_part_return__unordered(self, restore):
call(jkey, 86400), call(tkey, 86400),
])
- @patch('celery.result.GroupResult.restore')
- def test_on_chord_part_return__ordered(self, restore):
+ def test_on_chord_part_return__ordered(self):
self.app.conf.result_backend_transport_options = dict(
result_chord_ordered=True,
)
@@ -694,8 +740,7 @@ def test_on_chord_part_return__ordered(self, restore):
call(jkey, 86400), call(tkey, 86400),
])
- @patch('celery.result.GroupResult.restore')
- def test_on_chord_part_return_no_expiry(self, restore):
+ def test_on_chord_part_return_no_expiry(self):
old_expires = self.b.expires
self.b.expires = None
tasks = [self.create_task(i) for i in range(10)]
@@ -712,8 +757,7 @@ def test_on_chord_part_return_no_expiry(self, restore):
self.b.expires = old_expires
- @patch('celery.result.GroupResult.restore')
- def test_on_chord_part_return_expire_set_to_zero(self, restore):
+ def test_on_chord_part_return_expire_set_to_zero(self):
old_expires = self.b.expires
self.b.expires = 0
tasks = [self.create_task(i) for i in range(10)]
@@ -730,8 +774,7 @@ def test_on_chord_part_return_expire_set_to_zero(self, restore):
self.b.expires = old_expires
- @patch('celery.result.GroupResult.restore')
- def test_on_chord_part_return_no_expiry__unordered(self, restore):
+ def test_on_chord_part_return_no_expiry__unordered(self):
self.app.conf.result_backend_transport_options = dict(
result_chord_ordered=False,
)
@@ -752,8 +795,7 @@ def test_on_chord_part_return_no_expiry__unordered(self, restore):
self.b.expires = old_expires
- @patch('celery.result.GroupResult.restore')
- def test_on_chord_part_return_no_expiry__ordered(self, restore):
+ def test_on_chord_part_return_no_expiry__ordered(self):
self.app.conf.result_backend_transport_options = dict(
result_chord_ordered=True,
)
@@ -926,39 +968,80 @@ def test_on_chord_part_return__other_error__ordered(self):
callback.id, exc=ANY,
)
- @contextmanager
- def chord_context(self, size=1):
- with patch('celery.backends.redis.maybe_signature') as ms:
- tasks = [self.create_task(i) for i in range(size)]
- request = Mock(name='request')
- request.id = 'id1'
- request.group = 'gid1'
- request.group_index = None
- callback = ms.return_value = Signature('add')
- callback.id = 'id1'
- callback['chord_size'] = size
- callback.delay = Mock(name='callback.delay')
- yield tasks, request, callback
- def test_process_cleanup(self):
- self.b.process_cleanup()
+class test_RedisBackend_chords_complex(basetest_RedisBackend):
+ @pytest.fixture(scope="function", autouse=True)
+ def complex_header_result(self):
+ with patch("celery.result.GroupResult.restore") as p:
+ yield p
+
+ def test_apply_chord_complex_header(self):
+ mock_header_result = Mock()
+ # No results in the header at all - won't call `save()`
+ mock_header_result.results = tuple()
+ self.b.apply_chord(mock_header_result, None)
+ mock_header_result.save.assert_not_called()
+ mock_header_result.save.reset_mock()
+ # A single simple result in the header - won't call `save()`
+ mock_header_result.results = (self.app.AsyncResult("foo"), )
+ self.b.apply_chord(mock_header_result, None)
+ mock_header_result.save.assert_not_called()
+ mock_header_result.save.reset_mock()
+ # Many simple results in the header - won't call `save()`
+ mock_header_result.results = (self.app.AsyncResult("foo"), ) * 42
+ self.b.apply_chord(mock_header_result, None)
+ mock_header_result.save.assert_not_called()
+ mock_header_result.save.reset_mock()
+ # A single complex result in the header - will call `save()`
+ mock_header_result.results = (self.app.GroupResult("foo"), )
+ self.b.apply_chord(mock_header_result, None)
+ mock_header_result.save.assert_called_once_with(backend=self.b)
+ mock_header_result.save.reset_mock()
+ # Many complex results in the header - will call `save()`
+ mock_header_result.results = (self.app.GroupResult("foo"), ) * 42
+ self.b.apply_chord(mock_header_result, None)
+ mock_header_result.save.assert_called_once_with(backend=self.b)
+ mock_header_result.save.reset_mock()
+ # Mixed simple and complex results in the header - will call `save()`
+ mock_header_result.results = itertools.islice(
+ itertools.cycle((
+ self.app.AsyncResult("foo"), self.app.GroupResult("foo"),
+ )), 42,
+ )
+ self.b.apply_chord(mock_header_result, None)
+ mock_header_result.save.assert_called_once_with(backend=self.b)
+ mock_header_result.save.reset_mock()
- def test_get_set_forget(self):
- tid = uuid()
- self.b.store_result(tid, 42, states.SUCCESS)
- assert self.b.get_state(tid) == states.SUCCESS
- assert self.b.get_result(tid) == 42
- self.b.forget(tid)
- assert self.b.get_state(tid) == states.PENDING
+ @pytest.mark.parametrize("supports_native_join", (True, False))
+ def test_on_chord_part_return(
+ self, complex_header_result, supports_native_join,
+ ):
+ mock_result_obj = complex_header_result.return_value
+ mock_result_obj.supports_native_join = supports_native_join
- def test_set_expires(self):
- self.b = self.Backend(expires=512, app=self.app)
- tid = uuid()
- key = self.b.get_key_for_task(tid)
- self.b.store_result(tid, 42, states.SUCCESS)
- self.b.client.expire.assert_called_with(
- key, 512,
- )
+ tasks = [self.create_task(i) for i in range(10)]
+ random.shuffle(tasks)
+
+ with self.chord_context(10) as (tasks, request, callback):
+ for task, result_val in zip(tasks, itertools.cycle((42, ))):
+ self.b.on_chord_part_return(
+ task.request, states.SUCCESS, result_val,
+ )
+ # Confirm that `zadd` was called even though we won't end up
+ # using the data pushed into the sorted set
+ assert self.b.client.zadd.call_count == 1
+ self.b.client.zadd.reset_mock()
+ # Confirm that neither `zrange` not `lrange` were called
+ self.b.client.zrange.assert_not_called()
+ self.b.client.lrange.assert_not_called()
+ # Confirm that the `GroupResult.restore` mock was called
+ complex_header_result.assert_called_once_with(request.group)
+ # Confirm the the callback was called with the `join()`ed group result
+ if supports_native_join:
+ expected_join = mock_result_obj.join_native
+ else:
+ expected_join = mock_result_obj.join
+ callback.delay.assert_called_once_with(expected_join())
class test_SentinelBackend:
From b27ac4ab07859b987848f02a5e5ef0c0853ee9da Mon Sep 17 00:00:00 2001
From: Lewis Kabui
Date: Wed, 14 Oct 2020 12:12:24 +0300
Subject: [PATCH 032/415] Update obsolete --loglevel argument values in docs
---
docs/getting-started/first-steps-with-celery.rst | 2 +-
docs/userguide/periodic-tasks.rst | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/getting-started/first-steps-with-celery.rst b/docs/getting-started/first-steps-with-celery.rst
index f3b80c42dbe..aefaa4aa867 100644
--- a/docs/getting-started/first-steps-with-celery.rst
+++ b/docs/getting-started/first-steps-with-celery.rst
@@ -159,7 +159,7 @@ argument:
.. code-block:: console
- $ celery -A tasks worker --loglevel=info
+ $ celery -A tasks worker --loglevel=INFO
.. note::
diff --git a/docs/userguide/periodic-tasks.rst b/docs/userguide/periodic-tasks.rst
index e68bcd26c50..1e346ed2557 100644
--- a/docs/userguide/periodic-tasks.rst
+++ b/docs/userguide/periodic-tasks.rst
@@ -463,7 +463,7 @@ To install and use this extension:
.. code-block:: console
- $ celery -A proj beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
+ $ celery -A proj beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler
Note: You may also add this as the :setting:`beat_scheduler` setting directly.
From f2825b4918d55a40e6f839f058ee353db3b90b36 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 15 Oct 2020 12:22:38 +0300
Subject: [PATCH 033/415] Set logfile, not loglevel.
---
extra/systemd/celery.service | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/extra/systemd/celery.service b/extra/systemd/celery.service
index 2510fb83cb0..ff6bacb89ed 100644
--- a/extra/systemd/celery.service
+++ b/extra/systemd/celery.service
@@ -12,7 +12,7 @@ ExecStart=/bin/sh -c '${CELERY_BIN} -A $CELERY_APP multi start $CELERYD_NODES \
--pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
--loglevel="${CELERYD_LOG_LEVEL}" $CELERYD_OPTS'
ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait $CELERYD_NODES \
- --pidfile=${CELERYD_PID_FILE} --loglevel="${CELERYD_LOG_LEVEL}"'
+ --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE}'
ExecReload=/bin/sh -c '${CELERY_BIN} -A $CELERY_APP multi restart $CELERYD_NODES \
--pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} \
--loglevel="${CELERYD_LOG_LEVEL}" $CELERYD_OPTS'
From 05da357502a109c05b35392391299d75d181ccab Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 15 Oct 2020 12:24:45 +0300
Subject: [PATCH 034/415] Mention removed deprecated modules in the release
notes.
Fixes #6406.
---
docs/whatsnew-5.0.rst | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/docs/whatsnew-5.0.rst b/docs/whatsnew-5.0.rst
index af8dd18fa5d..9360a5b9588 100644
--- a/docs/whatsnew-5.0.rst
+++ b/docs/whatsnew-5.0.rst
@@ -250,6 +250,18 @@ AMQP Result Backend
The AMQP result backend has been removed as it was deprecated in version 4.0.
+Removed Deprecated Modules
+--------------------------
+
+The `celery.utils.encoding` and the `celery.task` modules has been deprecated
+in version 4.0 and therefore are removed in 5.0.
+
+If you were using the `celery.utils.encoding` module before,
+you should import `kombu.utils.encoding` instead.
+
+If you were using the `celery.task` module before, you should import directly
+from the `celery` module instead.
+
.. _new_command_line_interface:
New Command Line Interface
From d1305f3e45dca17ea0c0c025a3a77c6aa62ec71a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Pa=CC=88rsson?=
Date: Fri, 16 Oct 2020 11:00:23 +0200
Subject: [PATCH 035/415] Copy __annotations__ when creating tasks
This will allow getting type hints.
Fixes #6186.
---
celery/app/base.py | 1 +
t/unit/app/test_app.py | 10 ++++++++++
2 files changed, 11 insertions(+)
diff --git a/celery/app/base.py b/celery/app/base.py
index dc7c41d804f..3e33bb068e1 100644
--- a/celery/app/base.py
+++ b/celery/app/base.py
@@ -428,6 +428,7 @@ def _task_from_fun(self, fun, name=None, base=None, bind=False, **options):
'_decorated': True,
'__doc__': fun.__doc__,
'__module__': fun.__module__,
+ '__annotations__': fun.__annotations__,
'__header__': staticmethod(head_from_fun(fun, bound=bind)),
'__wrapped__': run}, **options))()
# for some reason __qualname__ cannot be set in type()
diff --git a/t/unit/app/test_app.py b/t/unit/app/test_app.py
index 9571b401254..969489fa164 100644
--- a/t/unit/app/test_app.py
+++ b/t/unit/app/test_app.py
@@ -494,6 +494,16 @@ def foo():
finally:
_imports.MP_MAIN_FILE = None
+ def test_can_get_type_hints_for_tasks(self):
+ import typing
+
+ with self.Celery() as app:
+ @app.task
+ def foo(parameter: int) -> None:
+ pass
+
+ assert typing.get_type_hints(foo) == {'parameter': int, 'return': type(None)}
+
def test_annotate_decorator(self):
from celery.app.task import Task
From b57ac624b871e31db2994610e119720a2e167a2c Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Thu, 17 Sep 2020 11:13:44 +1000
Subject: [PATCH 036/415] test: Improve chord body group index freezing test
Add more elements to the body so we can verify that the `group_index`
counts up from 0 as expected. This change adds the `pytest-subtests`
package as a test dependency so we can define partially independent
subtests within test functions.
---
requirements/test.txt | 1 +
t/unit/tasks/test_canvas.py | 36 +++++++++++++++++++++++++-----------
2 files changed, 26 insertions(+), 11 deletions(-)
diff --git a/requirements/test.txt b/requirements/test.txt
index 8d338510e71..92ed354e4c8 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,6 +1,7 @@
case>=1.3.1
pytest~=6.0
pytest-celery
+pytest-subtests
pytest-timeout~=1.4.2
boto3>=1.9.178
moto==1.3.7
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
index f51efab9389..874339c4687 100644
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -2,6 +2,7 @@
from unittest.mock import MagicMock, Mock, call, patch, sentinel
import pytest
+import pytest_subtests # noqa: F401
from celery._state import _task_stack
from celery.canvas import (Signature, _chain, _maybe_group, chain, chord,
@@ -1005,22 +1006,35 @@ def test_repr(self):
x.kwargs['body'] = None
assert 'without body' in repr(x)
- def test_freeze_tasks_body_is_group(self):
- # Confirm that `group index` is passed from a chord to elements of its
- # body when the chord itself is encapsulated in a group
+ def test_freeze_tasks_body_is_group(self, subtests):
+ # Confirm that `group index` values counting up from 0 are set for
+ # elements of a chord's body when the chord is encapsulated in a group
body_elem = self.add.s()
- chord_body = group([body_elem])
+ chord_body = group([body_elem] * 42)
chord_obj = chord(self.add.s(), body=chord_body)
top_group = group([chord_obj])
# We expect the body to be the signature we passed in before we freeze
- (embedded_body_elem, ) = chord_obj.body.tasks
- assert embedded_body_elem is body_elem
- assert embedded_body_elem.options == dict()
- # When we freeze the chord, its body will be clones and options set
+ with subtests.test(msg="Validate body tasks are retained"):
+ assert all(
+ embedded_body_elem is body_elem
+ for embedded_body_elem in chord_obj.body.tasks
+ )
+ # We also expect the body to have no initial options - since all of the
+ # embedded body elements are confirmed to be `body_elem` this is valid
+ assert body_elem.options == {}
+ # When we freeze the chord, its body will be cloned and options set
top_group.freeze()
- (embedded_body_elem, ) = chord_obj.body.tasks
- assert embedded_body_elem is not body_elem
- assert embedded_body_elem.options["group_index"] == 0 # 0th task
+ with subtests.test(
+ msg="Validate body group indicies count from 0 after freezing"
+ ):
+ assert all(
+ embedded_body_elem is not body_elem
+ for embedded_body_elem in chord_obj.body.tasks
+ )
+ assert all(
+ embedded_body_elem.options["group_index"] == i
+ for i, embedded_body_elem in enumerate(chord_obj.body.tasks)
+ )
def test_freeze_tasks_is_not_group(self):
x = chord([self.add.s(2, 2)], body=self.add.s(), app=self.app)
From 89d50f5a34e3c9a16f6fd2cace4ac0dc214493dc Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Thu, 15 Oct 2020 09:21:59 +1100
Subject: [PATCH 037/415] test: Use all() for subtask checks in canvas tests
When we expect all of the tasks in some iterable to meet a conditional,
we should make that clear by using `all(condition for ...)`.
---
t/unit/tasks/test_canvas.py | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
index 874339c4687..23c805d157a 100644
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -327,13 +327,9 @@ def test_from_dict_no_tasks(self):
def test_from_dict_full_subtasks(self):
c = chain(self.add.si(1, 2), self.add.si(3, 4), self.add.si(5, 6))
-
serialized = json.loads(json.dumps(c))
-
deserialized = chain.from_dict(serialized)
-
- for task in deserialized.tasks:
- assert isinstance(task, Signature)
+ assert all(isinstance(task, Signature) for task in deserialized.tasks)
@pytest.mark.usefixtures('depends_on_current_app')
def test_app_falls_back_to_default(self):
@@ -346,9 +342,8 @@ def test_handles_dicts(self):
)
c.freeze()
tasks, _ = c._frozen
- for task in tasks:
- assert isinstance(task, Signature)
- assert task.app is self.app
+ assert all(isinstance(task, Signature) for task in tasks)
+ assert all(task.app is self.app for task in tasks)
def test_groups_in_chain_to_chord(self):
g1 = group([self.add.s(2, 2), self.add.s(4, 4)])
From 56acb7b22be559255e3e481851a8125726cbb4a9 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Wed, 16 Sep 2020 14:54:06 +1000
Subject: [PATCH 038/415] test: Add more tests for `from_dict()` variants
Notably, this exposed the bug tracked in #6341 where groups are not
deeply deserialized by `group.from_dict()`.
---
t/integration/tasks.py | 66 ++++++++++++++++-
t/integration/test_canvas.py | 102 +++++++++++++++++++++++++
t/unit/tasks/test_canvas.py | 139 +++++++++++++++++++++++++++++++++++
3 files changed, 306 insertions(+), 1 deletion(-)
diff --git a/t/integration/tasks.py b/t/integration/tasks.py
index 629afaf2ece..1b4bb581b0c 100644
--- a/t/integration/tasks.py
+++ b/t/integration/tasks.py
@@ -1,6 +1,6 @@
from time import sleep
-from celery import Task, chain, chord, group, shared_task
+from celery import Signature, Task, chain, chord, group, shared_task
from celery.exceptions import SoftTimeLimitExceeded
from celery.utils.log import get_task_logger
@@ -244,3 +244,67 @@ def run(self):
if self.request.retries:
return self.request.retries
raise ValueError()
+
+
+# The signatures returned by these tasks wouldn't actually run because the
+# arguments wouldn't be fulfilled - we never actually delay them so it's fine
+@shared_task
+def return_nested_signature_chain_chain():
+ return chain(chain([add.s()]))
+
+
+@shared_task
+def return_nested_signature_chain_group():
+ return chain(group([add.s()]))
+
+
+@shared_task
+def return_nested_signature_chain_chord():
+ return chain(chord([add.s()], add.s()))
+
+
+@shared_task
+def return_nested_signature_group_chain():
+ return group(chain([add.s()]))
+
+
+@shared_task
+def return_nested_signature_group_group():
+ return group(group([add.s()]))
+
+
+@shared_task
+def return_nested_signature_group_chord():
+ return group(chord([add.s()], add.s()))
+
+
+@shared_task
+def return_nested_signature_chord_chain():
+ return chord(chain([add.s()]), add.s())
+
+
+@shared_task
+def return_nested_signature_chord_group():
+ return chord(group([add.s()]), add.s())
+
+
+@shared_task
+def return_nested_signature_chord_chord():
+ return chord(chord([add.s()], add.s()), add.s())
+
+
+@shared_task
+def rebuild_signature(sig_dict):
+ sig_obj = Signature.from_dict(sig_dict)
+
+ def _recurse(sig):
+ if not isinstance(sig, Signature):
+ raise TypeError("{!r} is not a signature object".format(sig))
+ # Most canvas types have a `tasks` attribute
+ if isinstance(sig, (chain, group, chord)):
+ for task in sig.tasks:
+ _recurse(task)
+ # `chord`s also have a `body` attribute
+ if isinstance(sig, chord):
+ _recurse(sig.body)
+ _recurse(sig_obj)
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 256ecdbd9ee..2de8c0aa428 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -9,6 +9,7 @@
from celery.exceptions import TimeoutError
from celery.result import AsyncResult, GroupResult, ResultSet
+from . import tasks
from .conftest import get_active_redis_channels, get_redis_connection
from .tasks import (ExpectedException, add, add_chord_to_chord, add_replaced,
add_to_all, add_to_all_to_chord, build_chain_inside_task,
@@ -1095,3 +1096,104 @@ def test_nested_chord_group_chain_group_tail(self, manager):
)
res = sig.delay()
assert res.get(timeout=TIMEOUT) == [[42, 42]]
+
+
+class test_signature_serialization:
+ """
+ Confirm nested signatures can be rebuilt after passing through a backend.
+
+ These tests are expected to finish and return `None` or raise an exception
+ in the error case. The exception indicates that some element of a nested
+ signature object was not properly deserialized from its dictionary
+ representation, and would explode later on if it were used as a signature.
+ """
+ def test_rebuild_nested_chain_chain(self, manager):
+ sig = chain(
+ tasks.return_nested_signature_chain_chain.s(),
+ tasks.rebuild_signature.s()
+ )
+ sig.delay().get(timeout=TIMEOUT)
+
+ def test_rebuild_nested_chain_group(self, manager):
+ sig = chain(
+ tasks.return_nested_signature_chain_group.s(),
+ tasks.rebuild_signature.s()
+ )
+ sig.delay().get(timeout=TIMEOUT)
+
+ def test_rebuild_nested_chain_chord(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ sig = chain(
+ tasks.return_nested_signature_chain_chord.s(),
+ tasks.rebuild_signature.s()
+ )
+ sig.delay().get(timeout=TIMEOUT)
+
+ @pytest.mark.xfail(reason="#6341")
+ def test_rebuild_nested_group_chain(self, manager):
+ sig = chain(
+ tasks.return_nested_signature_group_chain.s(),
+ tasks.rebuild_signature.s()
+ )
+ sig.delay().get(timeout=TIMEOUT)
+
+ @pytest.mark.xfail(reason="#6341")
+ def test_rebuild_nested_group_group(self, manager):
+ sig = chain(
+ tasks.return_nested_signature_group_group.s(),
+ tasks.rebuild_signature.s()
+ )
+ sig.delay().get(timeout=TIMEOUT)
+
+ @pytest.mark.xfail(reason="#6341")
+ def test_rebuild_nested_group_chord(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ sig = chain(
+ tasks.return_nested_signature_group_chord.s(),
+ tasks.rebuild_signature.s()
+ )
+ sig.delay().get(timeout=TIMEOUT)
+
+ def test_rebuild_nested_chord_chain(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ sig = chain(
+ tasks.return_nested_signature_chord_chain.s(),
+ tasks.rebuild_signature.s()
+ )
+ sig.delay().get(timeout=TIMEOUT)
+
+ def test_rebuild_nested_chord_group(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ sig = chain(
+ tasks.return_nested_signature_chord_group.s(),
+ tasks.rebuild_signature.s()
+ )
+ sig.delay().get(timeout=TIMEOUT)
+
+ def test_rebuild_nested_chord_chord(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ sig = chain(
+ tasks.return_nested_signature_chord_chord.s(),
+ tasks.rebuild_signature.s()
+ )
+ sig.delay().get(timeout=TIMEOUT)
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
index 23c805d157a..32c0af1db10 100644
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -694,6 +694,32 @@ def test_from_dict(self):
x['args'] = None
assert group.from_dict(dict(x))
+ @pytest.mark.xfail(reason="#6341")
+ def test_from_dict_deep_deserialize(self):
+ original_group = group([self.add.s(1, 2)] * 42)
+ serialized_group = json.loads(json.dumps(original_group))
+ deserialized_group = group.from_dict(serialized_group)
+ assert all(
+ isinstance(child_task, Signature)
+ for child_task in deserialized_group.tasks
+ )
+
+ @pytest.mark.xfail(reason="#6341")
+ def test_from_dict_deeper_deserialize(self):
+ inner_group = group([self.add.s(1, 2)] * 42)
+ outer_group = group([inner_group] * 42)
+ serialized_group = json.loads(json.dumps(outer_group))
+ deserialized_group = group.from_dict(serialized_group)
+ assert all(
+ isinstance(child_task, Signature)
+ for child_task in deserialized_group.tasks
+ )
+ assert all(
+ isinstance(grandchild_task, Signature)
+ for child_task in deserialized_group.tasks
+ for grandchild_task in child_task.tasks
+ )
+
def test_call_empty_group(self):
x = group(app=self.app)
assert not len(x())
@@ -1059,6 +1085,119 @@ def chord_add():
_state.task_join_will_block = fixture_task_join_will_block
result.task_join_will_block = fixture_task_join_will_block
+ def test_from_dict(self):
+ header = self.add.s(1, 2)
+ original_chord = chord(header=header)
+ rebuilt_chord = chord.from_dict(dict(original_chord))
+ assert isinstance(rebuilt_chord, chord)
+
+ def test_from_dict_with_body(self):
+ header = body = self.add.s(1, 2)
+ original_chord = chord(header=header, body=body)
+ rebuilt_chord = chord.from_dict(dict(original_chord))
+ assert isinstance(rebuilt_chord, chord)
+
+ def test_from_dict_deep_deserialize(self, subtests):
+ header = body = self.add.s(1, 2)
+ original_chord = chord(header=header, body=body)
+ serialized_chord = json.loads(json.dumps(original_chord))
+ deserialized_chord = chord.from_dict(serialized_chord)
+ with subtests.test(msg="Verify chord is deserialized"):
+ assert isinstance(deserialized_chord, chord)
+ with subtests.test(msg="Validate chord header tasks is deserialized"):
+ assert all(
+ isinstance(child_task, Signature)
+ for child_task in deserialized_chord.tasks
+ )
+ with subtests.test(msg="Verify chord body is deserialized"):
+ assert isinstance(deserialized_chord.body, Signature)
+
+ @pytest.mark.xfail(reason="#6341")
+ def test_from_dict_deep_deserialize_group(self, subtests):
+ header = body = group([self.add.s(1, 2)] * 42)
+ original_chord = chord(header=header, body=body)
+ serialized_chord = json.loads(json.dumps(original_chord))
+ deserialized_chord = chord.from_dict(serialized_chord)
+ with subtests.test(msg="Verify chord is deserialized"):
+ assert isinstance(deserialized_chord, chord)
+ # A header which is a group gets unpacked into the chord's `tasks`
+ with subtests.test(
+ msg="Validate chord header tasks are deserialized and unpacked"
+ ):
+ assert all(
+ isinstance(child_task, Signature)
+ and not isinstance(child_task, group)
+ for child_task in deserialized_chord.tasks
+ )
+ # A body which is a group remains as it we passed in
+ with subtests.test(
+ msg="Validate chord body is deserialized and not unpacked"
+ ):
+ assert isinstance(deserialized_chord.body, group)
+ assert all(
+ isinstance(body_child_task, Signature)
+ for body_child_task in deserialized_chord.body.tasks
+ )
+
+ @pytest.mark.xfail(reason="#6341")
+ def test_from_dict_deeper_deserialize_group(self, subtests):
+ inner_group = group([self.add.s(1, 2)] * 42)
+ header = body = group([inner_group] * 42)
+ original_chord = chord(header=header, body=body)
+ serialized_chord = json.loads(json.dumps(original_chord))
+ deserialized_chord = chord.from_dict(serialized_chord)
+ with subtests.test(msg="Verify chord is deserialized"):
+ assert isinstance(deserialized_chord, chord)
+ # A header which is a group gets unpacked into the chord's `tasks`
+ with subtests.test(
+ msg="Validate chord header tasks are deserialized and unpacked"
+ ):
+ assert all(
+ isinstance(child_task, group)
+ for child_task in deserialized_chord.tasks
+ )
+ assert all(
+ isinstance(grandchild_task, Signature)
+ for child_task in deserialized_chord.tasks
+ for grandchild_task in child_task.tasks
+ )
+ # A body which is a group remains as it we passed in
+ with subtests.test(
+ msg="Validate chord body is deserialized and not unpacked"
+ ):
+ assert isinstance(deserialized_chord.body, group)
+ assert all(
+ isinstance(body_child_task, group)
+ for body_child_task in deserialized_chord.body.tasks
+ )
+ assert all(
+ isinstance(body_grandchild_task, Signature)
+ for body_child_task in deserialized_chord.body.tasks
+ for body_grandchild_task in body_child_task.tasks
+ )
+
+ def test_from_dict_deep_deserialize_chain(self, subtests):
+ header = body = chain([self.add.s(1, 2)] * 42)
+ original_chord = chord(header=header, body=body)
+ serialized_chord = json.loads(json.dumps(original_chord))
+ deserialized_chord = chord.from_dict(serialized_chord)
+ with subtests.test(msg="Verify chord is deserialized"):
+ assert isinstance(deserialized_chord, chord)
+ # A header which is a chain gets unpacked into the chord's `tasks`
+ with subtests.test(
+ msg="Validate chord header tasks are deserialized and unpacked"
+ ):
+ assert all(
+ isinstance(child_task, Signature)
+ and not isinstance(child_task, chain)
+ for child_task in deserialized_chord.tasks
+ )
+ # A body which is a chain gets mutatated into the hidden `_chain` class
+ with subtests.test(
+ msg="Validate chord body is deserialized and not unpacked"
+ ):
+ assert isinstance(deserialized_chord.body, _chain)
+
class test_maybe_signature(CanvasCase):
From 6957f960a9f398995e17c28b77e0d402137d8455 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Tue, 8 Sep 2020 13:26:38 +1000
Subject: [PATCH 039/415] fix: Ensure group tasks are deeply deserialised
Fixes #6341
---
celery/canvas.py | 10 +++++++++-
t/integration/test_canvas.py | 3 ---
t/unit/tasks/test_canvas.py | 4 ----
3 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/celery/canvas.py b/celery/canvas.py
index 2150d0e872d..0279965d2ee 100644
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -1047,8 +1047,16 @@ class group(Signature):
@classmethod
def from_dict(cls, d, app=None):
+ # We need to mutate the `kwargs` element in place to avoid confusing
+ # `freeze()` implementations which end up here and expect to be able to
+ # access elements from that dictionary later and refer to objects
+ # canonicalized here
+ orig_tasks = d["kwargs"]["tasks"]
+ d["kwargs"]["tasks"] = rebuilt_tasks = type(orig_tasks)((
+ maybe_signature(task, app=app) for task in orig_tasks
+ ))
return _upgrade(
- d, group(d['kwargs']['tasks'], app=app, **d['options']),
+ d, group(rebuilt_tasks, app=app, **d['options']),
)
def __init__(self, *tasks, **options):
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 2de8c0aa428..a07da12d95d 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -1133,7 +1133,6 @@ def test_rebuild_nested_chain_chord(self, manager):
)
sig.delay().get(timeout=TIMEOUT)
- @pytest.mark.xfail(reason="#6341")
def test_rebuild_nested_group_chain(self, manager):
sig = chain(
tasks.return_nested_signature_group_chain.s(),
@@ -1141,7 +1140,6 @@ def test_rebuild_nested_group_chain(self, manager):
)
sig.delay().get(timeout=TIMEOUT)
- @pytest.mark.xfail(reason="#6341")
def test_rebuild_nested_group_group(self, manager):
sig = chain(
tasks.return_nested_signature_group_group.s(),
@@ -1149,7 +1147,6 @@ def test_rebuild_nested_group_group(self, manager):
)
sig.delay().get(timeout=TIMEOUT)
- @pytest.mark.xfail(reason="#6341")
def test_rebuild_nested_group_chord(self, manager):
try:
manager.app.backend.ensure_chords_allowed()
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
index 32c0af1db10..b6bd7f94cea 100644
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -694,7 +694,6 @@ def test_from_dict(self):
x['args'] = None
assert group.from_dict(dict(x))
- @pytest.mark.xfail(reason="#6341")
def test_from_dict_deep_deserialize(self):
original_group = group([self.add.s(1, 2)] * 42)
serialized_group = json.loads(json.dumps(original_group))
@@ -704,7 +703,6 @@ def test_from_dict_deep_deserialize(self):
for child_task in deserialized_group.tasks
)
- @pytest.mark.xfail(reason="#6341")
def test_from_dict_deeper_deserialize(self):
inner_group = group([self.add.s(1, 2)] * 42)
outer_group = group([inner_group] * 42)
@@ -1112,7 +1110,6 @@ def test_from_dict_deep_deserialize(self, subtests):
with subtests.test(msg="Verify chord body is deserialized"):
assert isinstance(deserialized_chord.body, Signature)
- @pytest.mark.xfail(reason="#6341")
def test_from_dict_deep_deserialize_group(self, subtests):
header = body = group([self.add.s(1, 2)] * 42)
original_chord = chord(header=header, body=body)
@@ -1139,7 +1136,6 @@ def test_from_dict_deep_deserialize_group(self, subtests):
for body_child_task in deserialized_chord.body.tasks
)
- @pytest.mark.xfail(reason="#6341")
def test_from_dict_deeper_deserialize_group(self, subtests):
inner_group = group([self.add.s(1, 2)] * 42)
header = body = group([inner_group] * 42)
From 9b78de840d74d3e5cd6d4d7701ad64ba4a43fbe6 Mon Sep 17 00:00:00 2001
From: Lewis Kabui
Date: Sat, 17 Oct 2020 14:36:32 +0300
Subject: [PATCH 040/415] Fix `celery shell` command
---
celery/bin/shell.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/celery/bin/shell.py b/celery/bin/shell.py
index 966773c5d11..b3b77e02fdb 100644
--- a/celery/bin/shell.py
+++ b/celery/bin/shell.py
@@ -130,7 +130,7 @@ def shell(ctx, ipython=False, bpython=False,
import_module('celery.concurrency.eventlet')
if gevent:
import_module('celery.concurrency.gevent')
- import celery.task.base
+ import celery
app = ctx.obj.app
app.loader.import_default_modules()
From e966cf1be71766c763d884fa57cf45e7444de75c Mon Sep 17 00:00:00 2001
From: Anthony Lukach
Date: Thu, 15 Oct 2020 08:53:02 -0600
Subject: [PATCH 041/415] predefined_queues_urls -> predefined_queues
---
docs/getting-started/brokers/sqs.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/getting-started/brokers/sqs.rst b/docs/getting-started/brokers/sqs.rst
index 5b108cdc048..2e41ce4ef9e 100644
--- a/docs/getting-started/brokers/sqs.rst
+++ b/docs/getting-started/brokers/sqs.rst
@@ -137,7 +137,7 @@ Predefined Queues
If you want Celery to use a set of predefined queues in AWS, and to
never attempt to list SQS queues, nor attempt to create or delete them,
-pass a map of queue names to URLs using the :setting:`predefined_queue_urls`
+pass a map of queue names to URLs using the :setting:`predefined_queues`
setting::
broker_transport_options = {
From 387518c6b2b53f816c5a59facafe500b075f7c01 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 18 Oct 2020 17:32:10 +0300
Subject: [PATCH 042/415] Update changelog.
---
Changelog.rst | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/Changelog.rst b/Changelog.rst
index a8fc6d47665..14c2b0b0b4c 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -8,6 +8,35 @@ This document contains change notes for bugfix & new features
in the 5.0.x series, please see :ref:`whatsnew-5.0` for
an overview of what's new in Celery 5.0.
+.. _version-5.0.1:
+
+5.0.1
+=====
+:release-date: 2020-10-18 1.00 P.M UTC+3:00
+:release-by: Omer Katz
+
+- Specify UTF-8 as the encoding for log files (#6357).
+- Custom headers now propagate when using the protocol 1 hybrid messages (#6374).
+- Retry creating the database schema for the database results backend
+ in case of a race condition (#6298).
+- When using the Redis results backend, awaiting for a chord no longer hangs
+ when setting :setting:`result_expires` to 0 (#6373).
+- When a user tries to specify the app as an option for the subcommand,
+ a custom error message is displayed (#6363).
+- Fix the `--without-gossip`, `--without-mingle`, and `--without-heartbeat`
+ options which now work as expected. (#6365)
+- Provide a clearer error message when the application cannot be loaded.
+- Avoid printing deprecation warnings for settings when they are loaded from
+ Django settings (#6385).
+- Allow lowercase log levels for the `--loglevel` option (#6388).
+- Detaching now works as expected (#6401).
+- Restore broadcasting messages from `celery control` (#6400).
+- Pass back real result for single task chains (#6411).
+- Ensure group tasks a deeply serialized (#6342).
+- Fix chord element counting (#6354).
+- Restore the `celery shell` command (#6421).
+
+.. _version-5.0.0:
5.0.0
=====
From b50b178f41c798f63aad77c0e4908c8a7139a753 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 18 Oct 2020 17:34:42 +0300
Subject: [PATCH 043/415] =?UTF-8?q?Bump=20version:=205.0.0=20=E2=86=92=205?=
=?UTF-8?q?.0.1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.bumpversion.cfg | 2 +-
README.rst | 6 +++---
celery/__init__.py | 2 +-
docs/includes/introduction.txt | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 80aca1abc6f..ea5f7e924c0 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 5.0.0
+current_version = 5.0.1
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z\d]+)?
diff --git a/README.rst b/README.rst
index 3896f32a6fa..8cb4e40671b 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
|build-status| |coverage| |license| |wheel| |pyversion| |pyimp| |ocbackerbadge| |ocsponsorbadge|
-:Version: 5.0.0 (singularity)
+:Version: 5.0.1 (singularity)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
@@ -57,7 +57,7 @@ in such a way that the client enqueues an URL to be requested by a worker.
What do I need?
===============
-Celery version 5.0.0 runs on,
+Celery version 5.0.1 runs on,
- Python (3.6, 3.7, 3.8)
- PyPy3.6 (7.6)
@@ -89,7 +89,7 @@ Get Started
===========
If this is the first time you're trying to use Celery, or you're
-new to Celery 5.0.0 coming from previous versions then you should read our
+new to Celery 5.0.1 coming from previous versions then you should read our
getting started tutorials:
- `First steps with Celery`_
diff --git a/celery/__init__.py b/celery/__init__.py
index 9ccaae8874d..a9f497130e7 100644
--- a/celery/__init__.py
+++ b/celery/__init__.py
@@ -17,7 +17,7 @@
SERIES = 'singularity'
-__version__ = '5.0.0'
+__version__ = '5.0.1'
__author__ = 'Ask Solem'
__contact__ = 'auvipy@gmail.com'
__homepage__ = 'http://celeryproject.org'
diff --git a/docs/includes/introduction.txt b/docs/includes/introduction.txt
index 0ba1f965b3f..188fd291478 100644
--- a/docs/includes/introduction.txt
+++ b/docs/includes/introduction.txt
@@ -1,4 +1,4 @@
-:Version: 5.0.0 (cliffs)
+:Version: 5.0.1 (cliffs)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
From 76596a1892a2c5d826b3d5ffb16623d1b645bb6b Mon Sep 17 00:00:00 2001
From: Safwan Rahman
Date: Mon, 19 Oct 2020 16:10:57 +0600
Subject: [PATCH 044/415] [Fix #6361] Fixing documentation for RabbitMQ
task_queue_ha_policy
---
docs/userguide/configuration.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index 67b3bf96846..a9d0379972f 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -2122,8 +2122,8 @@ Or you can give it a list of nodes to replicate to:
task_queue_ha_policy = ['rabbit@host1', 'rabbit@host2']
-Using a list will implicitly set ``x-ha-policy`` to 'nodes' and
-``x-ha-policy-params`` to the given list of nodes.
+Using a list will implicitly set ``ha-mode`` to 'nodes' and
+``ha-params`` to the given list of nodes.
See http://www.rabbitmq.com/ha.html for more information.
From a9b1918ac670dd27a55f20ab37c86d8bc8454f3a Mon Sep 17 00:00:00 2001
From: Stepan Henek
Date: Mon, 19 Oct 2020 10:03:34 +0200
Subject: [PATCH 045/415] Fix _autodiscover_tasks_from_fixups function
---
celery/app/base.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/celery/app/base.py b/celery/app/base.py
index 3e33bb068e1..ab9433a8a4e 100644
--- a/celery/app/base.py
+++ b/celery/app/base.py
@@ -649,8 +649,8 @@ def _autodiscover_tasks_from_names(self, packages, related_name):
def _autodiscover_tasks_from_fixups(self, related_name):
return self._autodiscover_tasks_from_names([
pkg for fixup in self._fixups
- for pkg in fixup.autodiscover_tasks()
if hasattr(fixup, 'autodiscover_tasks')
+ for pkg in fixup.autodiscover_tasks()
], related_name=related_name)
def send_task(self, name, args=None, kwargs=None, countdown=None,
From 3187044b57335f37fe18f47f230efc0fb00f4d58 Mon Sep 17 00:00:00 2001
From: Stepan Henek
Date: Mon, 19 Oct 2020 22:53:42 +0200
Subject: [PATCH 046/415] fixup! Fix _autodiscover_tasks_from_fixups function
---
t/unit/app/test_app.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/t/unit/app/test_app.py b/t/unit/app/test_app.py
index 969489fa164..a533d0cc4d4 100644
--- a/t/unit/app/test_app.py
+++ b/t/unit/app/test_app.py
@@ -218,6 +218,13 @@ def test_using_v1_reduce(self):
self.app._using_v1_reduce = True
assert loads(dumps(self.app))
+ def test_autodiscover_tasks_force_fixup_fallback(self):
+ self.app.loader.autodiscover_tasks = Mock()
+ self.app.autodiscover_tasks([], force=True)
+ self.app.loader.autodiscover_tasks.assert_called_with(
+ [], 'tasks',
+ )
+
def test_autodiscover_tasks_force(self):
self.app.loader.autodiscover_tasks = Mock()
self.app.autodiscover_tasks(['proj.A', 'proj.B'], force=True)
From 215d3c1eb4c49ef1a6e89ce9d438ade638746a68 Mon Sep 17 00:00:00 2001
From: KexZh
Date: Wed, 21 Oct 2020 13:34:59 +1300
Subject: [PATCH 047/415] Correct configuration item: CELERY_RESULT_EXPIRES
Related issue: https://github.com/celery/celery/issues/4050
https://github.com/celery/celery/issues/4050#issuecomment-524626647
---
docs/userguide/configuration.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index a9d0379972f..5331c4b9a58 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -115,7 +115,7 @@ have been moved into a new ``task_`` prefix.
``CELERY_MESSAGE_COMPRESSION`` :setting:`result_compression`
``CELERY_RESULT_EXCHANGE`` :setting:`result_exchange`
``CELERY_RESULT_EXCHANGE_TYPE`` :setting:`result_exchange_type`
-``CELERY_TASK_RESULT_EXPIRES`` :setting:`result_expires`
+``CELERY_RESULT_EXPIRES`` :setting:`result_expires`
``CELERY_RESULT_PERSISTENT`` :setting:`result_persistent`
``CELERY_RESULT_SERIALIZER`` :setting:`result_serializer`
``CELERY_RESULT_DBURI`` Use :setting:`result_backend` instead.
From a2498d37aa40614a2eecb3dddcae61754056b5c9 Mon Sep 17 00:00:00 2001
From: Thomas Riccardi
Date: Thu, 22 Oct 2020 17:47:14 +0200
Subject: [PATCH 048/415] Flush worker prints, notably the banner
In some cases (kubernetes, root) the banner is only printed at the
end of the process execution, instead of at the beginning.
---
celery/apps/worker.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/celery/apps/worker.py b/celery/apps/worker.py
index 882751fb8a9..c220857eb3a 100644
--- a/celery/apps/worker.py
+++ b/celery/apps/worker.py
@@ -79,7 +79,7 @@ def active_thread_count():
def safe_say(msg):
- print(f'\n{msg}', file=sys.__stderr__)
+ print(f'\n{msg}', file=sys.__stderr__, flush=True)
class Worker(WorkController):
@@ -169,7 +169,7 @@ def emit_banner(self):
str(self.colored.cyan(
' \n', self.startup_info(artlines=not use_image))),
str(self.colored.reset(self.extra_info() or '')),
- ])), file=sys.__stdout__)
+ ])), file=sys.__stdout__, flush=True)
def on_consumer_ready(self, consumer):
signals.worker_ready.send(sender=consumer)
@@ -187,7 +187,7 @@ def purge_messages(self):
with self.app.connection_for_write() as connection:
count = self.app.control.purge(connection=connection)
if count: # pragma: no cover
- print(f"purge: Erased {count} {pluralize(count, 'message')} from the queue.\n")
+ print(f"purge: Erased {count} {pluralize(count, 'message')} from the queue.\n", flush=True)
def tasklist(self, include_builtins=True, sep='\n', int_='celery.'):
return sep.join(
From 8c5e9888ae10288ae1b2113bdce6a4a41c47354b Mon Sep 17 00:00:00 2001
From: Safwan Rahman
Date: Tue, 27 Oct 2020 02:34:51 +0600
Subject: [PATCH 049/415] [Fix #6361] Remove RabbitMQ ha_policy from queue
---
celery/app/amqp.py | 21 +++------------------
celery/app/defaults.py | 1 -
docs/userguide/configuration.rst | 27 ---------------------------
t/unit/app/test_amqp.py | 32 --------------------------------
4 files changed, 3 insertions(+), 78 deletions(-)
diff --git a/celery/app/amqp.py b/celery/app/amqp.py
index 7031bc8b9b6..1a0454e9a92 100644
--- a/celery/app/amqp.py
+++ b/celery/app/amqp.py
@@ -46,7 +46,6 @@ class Queues(dict):
create_missing (bool): By default any unknown queues will be
added automatically, but if this flag is disabled the occurrence
of unknown queues in `wanted` will raise :exc:`KeyError`.
- ha_policy (Sequence, str): Default HA policy for queues with none set.
max_priority (int): Default x-max-priority for queues with none set.
"""
@@ -55,14 +54,13 @@ class Queues(dict):
_consume_from = None
def __init__(self, queues=None, default_exchange=None,
- create_missing=True, ha_policy=None, autoexchange=None,
+ create_missing=True, autoexchange=None,
max_priority=None, default_routing_key=None):
dict.__init__(self)
self.aliases = WeakValueDictionary()
self.default_exchange = default_exchange
self.default_routing_key = default_routing_key
self.create_missing = create_missing
- self.ha_policy = ha_policy
self.autoexchange = Exchange if autoexchange is None else autoexchange
self.max_priority = max_priority
if queues is not None and not isinstance(queues, Mapping):
@@ -122,10 +120,6 @@ def _add(self, queue):
queue.exchange = self.default_exchange
if not queue.routing_key:
queue.routing_key = self.default_routing_key
- if self.ha_policy:
- if queue.queue_arguments is None:
- queue.queue_arguments = {}
- self._set_ha_policy(queue.queue_arguments)
if self.max_priority is not None:
if queue.queue_arguments is None:
queue.queue_arguments = {}
@@ -133,13 +127,6 @@ def _add(self, queue):
self[queue.name] = queue
return queue
- def _set_ha_policy(self, args):
- policy = self.ha_policy
- if isinstance(policy, (list, tuple)):
- return args.update({'ha-mode': 'nodes',
- 'ha-params': list(policy)})
- args['ha-mode'] = policy
-
def _set_max_priority(self, args):
if 'x-max-priority' not in args and self.max_priority is not None:
return args.update({'x-max-priority': self.max_priority})
@@ -251,7 +238,7 @@ def create_task_message(self):
def send_task_message(self):
return self._create_task_sender()
- def Queues(self, queues, create_missing=None, ha_policy=None,
+ def Queues(self, queues, create_missing=None,
autoexchange=None, max_priority=None):
# Create new :class:`Queues` instance, using queue defaults
# from the current configuration.
@@ -259,8 +246,6 @@ def Queues(self, queues, create_missing=None, ha_policy=None,
default_routing_key = conf.task_default_routing_key
if create_missing is None:
create_missing = conf.task_create_missing_queues
- if ha_policy is None:
- ha_policy = conf.task_queue_ha_policy
if max_priority is None:
max_priority = conf.task_queue_max_priority
if not queues and conf.task_default_queue:
@@ -271,7 +256,7 @@ def Queues(self, queues, create_missing=None, ha_policy=None,
else autoexchange)
return self.queues_cls(
queues, self.default_exchange, create_missing,
- ha_policy, autoexchange, max_priority, default_routing_key,
+ autoexchange, max_priority, default_routing_key,
)
def Router(self, queues=None, create_missing=None):
diff --git a/celery/app/defaults.py b/celery/app/defaults.py
index d0fa9d20b54..9fec8472c96 100644
--- a/celery/app/defaults.py
+++ b/celery/app/defaults.py
@@ -267,7 +267,6 @@ def __repr__(self):
type='dict', old={'celery_task_publish_retry_policy'},
),
queues=Option(type='dict'),
- queue_ha_policy=Option(None, type='string'),
queue_max_priority=Option(None, type='int'),
reject_on_worker_lost=Option(type='bool'),
remote_tracebacks=Option(False, type='bool'),
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index 5331c4b9a58..e9c1c76c151 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -2100,33 +2100,6 @@ The final routing options for ``tasks.add`` will become:
See :ref:`routers` for more examples.
-.. setting:: task_queue_ha_policy
-
-``task_queue_ha_policy``
-~~~~~~~~~~~~~~~~~~~~~~~~
-:brokers: RabbitMQ
-
-Default: :const:`None`.
-
-This will set the default HA policy for a queue, and the value
-can either be a string (usually ``all``):
-
-.. code-block:: python
-
- task_queue_ha_policy = 'all'
-
-Using 'all' will replicate the queue to all current nodes,
-Or you can give it a list of nodes to replicate to:
-
-.. code-block:: python
-
- task_queue_ha_policy = ['rabbit@host1', 'rabbit@host2']
-
-Using a list will implicitly set ``ha-mode`` to 'nodes' and
-``ha-params`` to the given list of nodes.
-
-See http://www.rabbitmq.com/ha.html for more information.
-
.. setting:: task_queue_max_priority
``task_queue_max_priority``
diff --git a/t/unit/app/test_amqp.py b/t/unit/app/test_amqp.py
index ee36c08e235..bc2d26d3680 100644
--- a/t/unit/app/test_amqp.py
+++ b/t/unit/app/test_amqp.py
@@ -89,23 +89,6 @@ def test_setitem_adds_default_exchange(self):
q['foo'] = queue
assert q['foo'].exchange == q.default_exchange
- @pytest.mark.parametrize('ha_policy,qname,q,qargs,expected', [
- (None, 'xyz', 'xyz', None, None),
- (None, 'xyz', 'xyz', {'x-foo': 'bar'}, {'x-foo': 'bar'}),
- ('all', 'foo', Queue('foo'), None, {'ha-mode': 'all'}),
- ('all', 'xyx2',
- Queue('xyx2', queue_arguments={'x-foo': 'bar'}),
- None,
- {'ha-mode': 'all', 'x-foo': 'bar'}),
- (['A', 'B', 'C'], 'foo', Queue('foo'), None, {
- 'ha-mode': 'nodes',
- 'ha-params': ['A', 'B', 'C']}),
- ])
- def test_with_ha_policy(self, ha_policy, qname, q, qargs, expected):
- queues = Queues(ha_policy=ha_policy, create_missing=False)
- queues.add(q, queue_arguments=qargs)
- assert queues[qname].queue_arguments == expected
-
def test_select_add(self):
q = Queues()
q.select(['foo', 'bar'])
@@ -118,11 +101,6 @@ def test_deselect(self):
q.deselect('bar')
assert sorted(q._consume_from.keys()) == ['foo']
- def test_with_ha_policy_compat(self):
- q = Queues(ha_policy='all')
- q.add('bar')
- assert q['bar'].queue_arguments == {'ha-mode': 'all'}
-
def test_add_default_exchange(self):
ex = Exchange('fff', 'fanout')
q = Queues(default_exchange=ex)
@@ -143,12 +121,6 @@ def test_alias(self):
({'max_priority': 10},
'moo', Queue('moo', queue_arguments=None),
{'x-max-priority': 10}),
- ({'ha_policy': 'all', 'max_priority': 5},
- 'bar', 'bar',
- {'ha-mode': 'all', 'x-max-priority': 5}),
- ({'ha_policy': 'all', 'max_priority': 5},
- 'xyx2', Queue('xyx2', queue_arguments={'x-max-priority': 2}),
- {'ha-mode': 'all', 'x-max-priority': 2}),
({'max_priority': None},
'foo2', 'foo2',
None),
@@ -255,10 +227,6 @@ def test_countdown_negative(self):
with pytest.raises(ValueError):
self.app.amqp.as_task_v2(uuid(), 'foo', countdown=-1232132323123)
- def test_Queues__with_ha_policy(self):
- x = self.app.amqp.Queues({}, ha_policy='all')
- assert x.ha_policy == 'all'
-
def test_Queues__with_max_priority(self):
x = self.app.amqp.Queues({}, max_priority=23)
assert x.max_priority == 23
From 678e422092381d19d88aa928ee308d9562c545d9 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Fri, 16 Oct 2020 09:33:42 +1100
Subject: [PATCH 050/415] ci: Fix TOXENV for pypy3 unit tests
Fixes #6409
---
.travis.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index 96fb6f4d872..dc7e1e3c6c5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -64,8 +64,9 @@ jobs:
- TOXENV=flake8,apicheck,configcheck,bandit
- CELERY_TOX_PARALLEL='--parallel --parallel-live'
stage: lint
+
- python: pypy3.6-7.3.1
- env: TOXENV=pypy3
+ env: TOXENV=pypy3-unit
stage: test
before_install:
From f95f56842640c8c9f4050a233cfbc051a07ee376 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Fri, 16 Oct 2020 09:38:06 +1100
Subject: [PATCH 051/415] ci: Move Python 3.9 test base from dev to release
---
.travis.yml | 6 +++---
tox.ini | 12 ++++++------
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index dc7e1e3c6c5..3c532ee95de 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,7 +5,7 @@ python:
- '3.6'
- '3.7'
- '3.8'
- - '3.9-dev'
+ - '3.9'
os:
- linux
stages:
@@ -25,9 +25,9 @@ env:
jobs:
fast_finish: true
allow_failures:
- - python: '3.9-dev'
+ - python: '3.9'
include:
- - python: '3.9-dev'
+ - python: '3.9'
env: MATRIX_TOXENV=integration-rabbitmq
stage: integration
diff --git a/tox.ini b/tox.ini
index 1b12965923a..8ec20b7a007 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
[tox]
envlist =
- {3.6,3.7,3.8,3.9-dev,pypy3}-unit
- {3.6,3.7,3.8,3.9-dev,pypy3}-integration-{rabbitmq,redis,dynamodb,azureblockblob,cache,cassandra,elasticsearch}
+ {3.6,3.7,3.8,3.9,pypy3}-unit
+ {3.6,3.7,3.8,3.9,pypy3}-integration-{rabbitmq,redis,dynamodb,azureblockblob,cache,cassandra,elasticsearch}
flake8
apicheck
@@ -14,9 +14,9 @@ deps=
-r{toxinidir}/requirements/test.txt
-r{toxinidir}/requirements/pkgutils.txt
- 3.6,3.7,3.8,3.9-dev: -r{toxinidir}/requirements/test-ci-default.txt
- 3.5,3.6,3.7,3.8,3.9-dev: -r{toxinidir}/requirements/docs.txt
- 3.6,3.7,3.8,3.9-dev: -r{toxinidir}/requirements/docs.txt
+ 3.6,3.7,3.8,3.9: -r{toxinidir}/requirements/test-ci-default.txt
+ 3.5,3.6,3.7,3.8,3.9: -r{toxinidir}/requirements/docs.txt
+ 3.6,3.7,3.8,3.9: -r{toxinidir}/requirements/docs.txt
pypy3: -r{toxinidir}/requirements/test-ci-base.txt
integration: -r{toxinidir}/requirements/test-integration.txt
@@ -63,7 +63,7 @@ basepython =
3.6: python3.6
3.7: python3.7
3.8: python3.8
- 3.9-dev: python3.9
+ 3.9: python3.9
pypy3: pypy3
flake8,apicheck,linkcheck,configcheck,bandit: python3.8
flakeplus: python2.7
From 7c3da03a07882ca86b801ad78dd509a67cba60af Mon Sep 17 00:00:00 2001
From: Egor Sergeevich Poderiagin
Date: Thu, 29 Oct 2020 18:16:46 +0700
Subject: [PATCH 052/415] docs: fix celery beat settings
---
docs/userguide/configuration.rst | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index e9c1c76c151..f942188d07d 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -65,11 +65,11 @@ have been moved into a new ``task_`` prefix.
``CELERY_IMPORTS`` :setting:`imports`
``CELERY_INCLUDE`` :setting:`include`
``CELERY_TIMEZONE`` :setting:`timezone`
-``CELERYBEAT_MAX_LOOP_INTERVAL`` :setting:`beat_max_loop_interval`
-``CELERYBEAT_SCHEDULE`` :setting:`beat_schedule`
-``CELERYBEAT_SCHEDULER`` :setting:`beat_scheduler`
-``CELERYBEAT_SCHEDULE_FILENAME`` :setting:`beat_schedule_filename`
-``CELERYBEAT_SYNC_EVERY`` :setting:`beat_sync_every`
+``CELERY_BEAT_MAX_LOOP_INTERVAL`` :setting:`beat_max_loop_interval`
+``CELERY_BEAT_SCHEDULE`` :setting:`beat_schedule`
+``CELERY_BEAT_SCHEDULER`` :setting:`beat_scheduler`
+``CELERY_BEAT_SCHEDULE_FILENAME`` :setting:`beat_schedule_filename`
+``CELERY_BEAT_SYNC_EVERY`` :setting:`beat_sync_every`
``BROKER_URL`` :setting:`broker_url`
``BROKER_TRANSPORT`` :setting:`broker_transport`
``BROKER_TRANSPORT_OPTIONS`` :setting:`broker_transport_options`
From 70dc29e2c0286151ef3f5e267a5e912ff932927a Mon Sep 17 00:00:00 2001
From: "Asif Saif Uddin (Auvi)"
Date: Sat, 31 Oct 2020 11:13:22 +0600
Subject: [PATCH 053/415] move to travis-ci.com
---
README.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.rst b/README.rst
index 8cb4e40671b..25b5e2c8e2b 100644
--- a/README.rst
+++ b/README.rst
@@ -498,9 +498,9 @@ file in the top distribution directory for the full license text.
.. # vim: syntax=rst expandtab tabstop=4 shiftwidth=4 shiftround
-.. |build-status| image:: https://secure.travis-ci.org/celery/celery.png?branch=master
+.. |build-status| image:: https://api.travis-ci.com/celery/celery.png?branch=master
:alt: Build status
- :target: https://travis-ci.org/celery/celery
+ :target: https://travis-ci.com/celery/celery
.. |coverage| image:: https://codecov.io/github/celery/celery/coverage.svg?branch=master
:target: https://codecov.io/github/celery/celery?branch=master
From 0db172ef3b6b1771c763e0ec7937bdba63dacbc8 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Mon, 2 Nov 2020 00:39:45 +1100
Subject: [PATCH 054/415] fix: Ensure default fairness maps to `SCHED_FAIR`
(#6447)
Fixes #6386
---
celery/concurrency/asynpool.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/celery/concurrency/asynpool.py b/celery/concurrency/asynpool.py
index 4d2dd1138d2..7ea3eb204c9 100644
--- a/celery/concurrency/asynpool.py
+++ b/celery/concurrency/asynpool.py
@@ -84,6 +84,7 @@ def unpack_from(fmt, iobuf, unpack=unpack): # noqa
SCHED_STRATEGIES = {
None: SCHED_STRATEGY_FAIR,
+ 'default': SCHED_STRATEGY_FAIR,
'fast': SCHED_STRATEGY_FCFS,
'fcfs': SCHED_STRATEGY_FCFS,
'fair': SCHED_STRATEGY_FAIR,
From 56fc486c02b8ec635ef930490ab751e7f582cc72 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 1 Nov 2020 16:19:23 +0200
Subject: [PATCH 055/415] Preserve callbacks when replacing a task with a chain
(#6189)
* Preserve callbacks when replacing a task with a chain.
* Preserve callbacks when replacing a task with a chain.
* Added tests.
* Update celery/app/task.py
Co-authored-by: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
* Mark test as flaky.
* Fix race condition in CI.
* fix: Run linked tasks in original slot for replace
This change alters the handling of linked tasks for chains which are
used as the argument to a `.replace()` call for a task which itself has
a chain of signatures to call once it completes. We ensure that the
linked callback is not only retained but also called at the appropiate
point in the newly reconstructed chain comprised of tasks from both the
replacement chain and the tail of the encapsulating chain of the task
being replaced.
We amend some tests to validate this behaviour better and ensure that
call/errbacks behave as expected if the encapsulating chain has either
set. One test is marked with an `xfail` since errbacks of encapsulating
chains are not currently called as expected due to some ambiguity in
when an errback of a replaced task should be dropped or not (#6441).
Co-authored-by: Asif Saif Uddin
Co-authored-by: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
---
celery/app/task.py | 11 +++
t/integration/tasks.py | 30 +++++++-
t/integration/test_canvas.py | 134 ++++++++++++++++++++++++++++++++++-
3 files changed, 171 insertions(+), 4 deletions(-)
diff --git a/celery/app/task.py b/celery/app/task.py
index 86c4e727d49..f8ffaefaffd 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -882,6 +882,17 @@ def replace(self, sig):
)
if self.request.chain:
+ # We need to freeze the new signature with the current task's ID to
+ # ensure that we don't disassociate the new chain from the existing
+ # task IDs which would break previously constructed results
+ # objects.
+ sig.freeze(self.request.id)
+ if "link" in sig.options:
+ final_task_links = sig.tasks[-1].options.setdefault("link", [])
+ final_task_links.extend(maybe_list(sig.options["link"]))
+ # Construct the new remainder of the task by chaining the signature
+ # we're being replaced by with signatures constructed from the
+ # chain elements in the current request.
for t in reversed(self.request.chain):
sig |= signature(t, app=self.app)
diff --git a/t/integration/tasks.py b/t/integration/tasks.py
index 1b4bb581b0c..8aa13bc1797 100644
--- a/t/integration/tasks.py
+++ b/t/integration/tasks.py
@@ -22,7 +22,7 @@ def add(x, y):
@shared_task
-def raise_error():
+def raise_error(*args):
"""Deliberately raise an error."""
raise ValueError("deliberate error")
@@ -76,6 +76,30 @@ def add_replaced(self, x, y):
raise self.replace(add.s(x, y))
+@shared_task(bind=True)
+def replace_with_chain(self, *args, link_msg=None):
+ c = chain(identity.s(*args), identity.s())
+ link_sig = redis_echo.s()
+ if link_msg is not None:
+ link_sig.args = (link_msg,)
+ link_sig.set(immutable=True)
+ c.link(link_sig)
+
+ return self.replace(c)
+
+
+@shared_task(bind=True)
+def replace_with_chain_which_raises(self, *args, link_msg=None):
+ c = chain(identity.s(*args), raise_error.s())
+ link_sig = redis_echo.s()
+ if link_msg is not None:
+ link_sig.args = (link_msg,)
+ link_sig.set(immutable=True)
+ c.link_error(link_sig)
+
+ return self.replace(c)
+
+
@shared_task(bind=True)
def add_to_all(self, nums, val):
"""Add the given value to all supplied numbers."""
@@ -143,7 +167,8 @@ def retry_once(self, *args, expires=60.0, max_retries=1, countdown=0.1):
@shared_task(bind=True, expires=60.0, max_retries=1)
-def retry_once_priority(self, *args, expires=60.0, max_retries=1, countdown=0.1):
+def retry_once_priority(self, *args, expires=60.0, max_retries=1,
+ countdown=0.1):
"""Task that fails and is retried. Returns the priority."""
if self.request.retries:
return self.request.delivery_info['priority']
@@ -160,7 +185,6 @@ def redis_echo(message):
@shared_task(bind=True)
def second_order_replace1(self, state=False):
-
redis_connection = get_redis_connection()
if not state:
redis_connection.rpush('redis-echo', 'In A')
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index a07da12d95d..4ae027fb10a 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -17,7 +17,7 @@
delayed_sum_with_soft_guard, fail, identity, ids,
print_unicode, raise_error, redis_echo, retry_once,
return_exception, return_priority, second_order_replace1,
- tsum)
+ tsum, replace_with_chain, replace_with_chain_which_raises)
RETRYABLE_EXCEPTIONS = (OSError, ConnectionError, TimeoutError)
@@ -414,6 +414,7 @@ def test_chain_of_a_chord_and_three_tasks_and_a_group(self, manager):
res = c()
assert res.get(timeout=TIMEOUT) == [8, 8]
+ @flaky
def test_nested_chain_group_lone(self, manager):
"""
Test that a lone group in a chain completes.
@@ -452,6 +453,137 @@ def test_nested_chain_group_last(self, manager):
res = sig.delay()
assert res.get(timeout=TIMEOUT) == [42, 42]
+ def test_chain_replaced_with_a_chain_and_a_callback(self, manager):
+ if not manager.app.conf.result_backend.startswith('redis'):
+ raise pytest.skip('Requires redis result backend.')
+
+ redis_connection = get_redis_connection()
+ redis_connection.delete('redis-echo')
+
+ link_msg = 'Internal chain callback'
+ c = chain(
+ identity.s('Hello '),
+ # The replacement chain will pass its args though
+ replace_with_chain.s(link_msg=link_msg),
+ add.s('world'),
+ )
+ res = c.delay()
+
+ assert res.get(timeout=TIMEOUT) == 'Hello world'
+
+ expected_msgs = {link_msg, }
+ while expected_msgs:
+ maybe_key_msg = redis_connection.blpop('redis-echo', TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError('redis-echo')
+ _, msg = maybe_key_msg
+ msg = msg.decode()
+ expected_msgs.remove(msg) # KeyError if `msg` is not in here
+
+ # There should be no more elements - block momentarily
+ assert redis_connection.blpop('redis-echo', min(1, TIMEOUT)) is None
+ redis_connection.delete('redis-echo')
+
+ def test_chain_replaced_with_a_chain_and_an_error_callback(self, manager):
+ if not manager.app.conf.result_backend.startswith('redis'):
+ raise pytest.skip('Requires redis result backend.')
+
+ redis_connection = get_redis_connection()
+ redis_connection.delete('redis-echo')
+
+ link_msg = 'Internal chain errback'
+ c = chain(
+ identity.s('Hello '),
+ replace_with_chain_which_raises.s(link_msg=link_msg),
+ add.s(' will never be seen :(')
+ )
+ res = c.delay()
+
+ with pytest.raises(ValueError):
+ res.get(timeout=TIMEOUT)
+
+ expected_msgs = {link_msg, }
+ while expected_msgs:
+ maybe_key_msg = redis_connection.blpop('redis-echo', TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError('redis-echo')
+ _, msg = maybe_key_msg
+ msg = msg.decode()
+ expected_msgs.remove(msg) # KeyError if `msg` is not in here
+
+ # There should be no more elements - block momentarily
+ assert redis_connection.blpop('redis-echo', min(1, TIMEOUT)) is None
+ redis_connection.delete('redis-echo')
+
+ def test_chain_with_cb_replaced_with_chain_with_cb(self, manager):
+ if not manager.app.conf.result_backend.startswith('redis'):
+ raise pytest.skip('Requires redis result backend.')
+
+ redis_connection = get_redis_connection()
+ redis_connection.delete('redis-echo')
+
+ link_msg = 'Internal chain callback'
+ c = chain(
+ identity.s('Hello '),
+ # The replacement chain will pass its args though
+ replace_with_chain.s(link_msg=link_msg),
+ add.s('world'),
+ )
+ c.link(redis_echo.s())
+ res = c.delay()
+
+ assert res.get(timeout=TIMEOUT) == 'Hello world'
+
+ expected_msgs = {link_msg, 'Hello world'}
+ while expected_msgs:
+ maybe_key_msg = redis_connection.blpop('redis-echo', TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError('redis-echo')
+ _, msg = maybe_key_msg
+ msg = msg.decode()
+ expected_msgs.remove(msg) # KeyError if `msg` is not in here
+
+ # There should be no more elements - block momentarily
+ assert redis_connection.blpop('redis-echo', min(1, TIMEOUT)) is None
+ redis_connection.delete('redis-echo')
+
+ @pytest.mark.xfail(reason="#6441")
+ def test_chain_with_eb_replaced_with_chain_with_eb(self, manager):
+ if not manager.app.conf.result_backend.startswith('redis'):
+ raise pytest.skip('Requires redis result backend.')
+
+ redis_connection = get_redis_connection()
+ redis_connection.delete('redis-echo')
+
+ inner_link_msg = 'Internal chain errback'
+ outer_link_msg = 'External chain errback'
+ c = chain(
+ identity.s('Hello '),
+ # The replacement chain will pass its args though
+ replace_with_chain_which_raises.s(link_msg=inner_link_msg),
+ add.s('world'),
+ )
+ c.link_error(redis_echo.s(outer_link_msg))
+ res = c.delay()
+
+ with pytest.raises(ValueError):
+ res.get(timeout=TIMEOUT)
+
+ expected_msgs = {inner_link_msg, outer_link_msg}
+ while expected_msgs:
+ # Shorter timeout here because we expect failure
+ timeout = min(5, TIMEOUT)
+ maybe_key_msg = redis_connection.blpop('redis-echo', timeout)
+ if maybe_key_msg is None:
+ raise TimeoutError('redis-echo')
+ _, msg = maybe_key_msg
+ msg = msg.decode()
+ expected_msgs.remove(msg) # KeyError if `msg` is not in here
+
+ # There should be no more elements - block momentarily
+ assert redis_connection.blpop('redis-echo', min(1, TIMEOUT)) is None
+ redis_connection.delete('redis-echo')
+
class test_result_set:
From f1145a2d91bd525f8e0f7a5662c9093e02fbf5a8 Mon Sep 17 00:00:00 2001
From: "Lewis M. Kabui" <13940255+lewisemm@users.noreply.github.com>
Date: Mon, 2 Nov 2020 13:56:45 +0300
Subject: [PATCH 056/415] Fix minor documentation omission (#6453)
Co-authored-by: Lewis Kabui
---
docs/userguide/monitoring.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/userguide/monitoring.rst b/docs/userguide/monitoring.rst
index 40e9991b572..725f264057f 100644
--- a/docs/userguide/monitoring.rst
+++ b/docs/userguide/monitoring.rst
@@ -33,7 +33,7 @@ To list all the commands available do:
.. code-block:: console
- $ celery help
+ $ celery --help
or to get help for a specific command do:
From 0833a270fae4738e128d56a63d0c4446ba0b1927 Mon Sep 17 00:00:00 2001
From: Ixiodor
Date: Mon, 2 Nov 2020 12:28:51 +0100
Subject: [PATCH 057/415] Fix max_retries override on self.retry (#6436)
* Fix max_retries override
* Fix max_retries override
* Fix max_retries override
* Update exceptions.py
typo
* Update autoretry.py
typo
* Update task.py
Prevent exception unpacking for tasks without autoretry_for
* Update test_tasks.py
Unit test
* Update test_tasks.py
Added a new test
* Update autoretry.py
Fox for explicit raise in tasks
* Update test_tasks.py
* Update autoretry.py
* Update task.py
* Update exceptions.py
* Update task.py
---
celery/app/autoretry.py | 11 ++++++++++-
celery/app/task.py | 2 ++
celery/exceptions.py | 1 +
t/unit/tasks/test_tasks.py | 37 +++++++++++++++++++++++++++++++++++++
4 files changed, 50 insertions(+), 1 deletion(-)
diff --git a/celery/app/autoretry.py b/celery/app/autoretry.py
index 21c90e026a2..5da8487bd5f 100644
--- a/celery/app/autoretry.py
+++ b/celery/app/autoretry.py
@@ -46,6 +46,15 @@ def run(*args, **kwargs):
retries=task.request.retries,
maximum=retry_backoff_max,
full_jitter=retry_jitter)
- raise task.retry(exc=exc, **retry_kwargs)
+ # Override max_retries
+ if hasattr(task, 'override_max_retries'):
+ retry_kwargs['max_retries'] = getattr(task,
+ 'override_max_retries',
+ task.max_retries)
+ ret = task.retry(exc=exc, **retry_kwargs)
+ # Stop propagation
+ if hasattr(task, 'override_max_retries'):
+ delattr(task, 'override_max_retries')
+ raise ret
task._orig_run, task.run = task.run, run
diff --git a/celery/app/task.py b/celery/app/task.py
index f8ffaefaffd..cab270cfa30 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -675,6 +675,8 @@ def retry(self, args=None, kwargs=None, exc=None, throw=True,
"""
request = self.request
retries = request.retries + 1
+ if max_retries is not None:
+ self.override_max_retries = max_retries
max_retries = self.max_retries if max_retries is None else max_retries
# Not in worker or emulated by (apply/always_eager),
diff --git a/celery/exceptions.py b/celery/exceptions.py
index 768cd4d22d2..ee903290f2f 100644
--- a/celery/exceptions.py
+++ b/celery/exceptions.py
@@ -293,3 +293,4 @@ def __init__(self, *args, **kwargs):
def __repr__(self):
return super().__repr__() + " state:" + self.state + " task_id:" + self.task_id
+
diff --git a/t/unit/tasks/test_tasks.py b/t/unit/tasks/test_tasks.py
index 154ee0295cb..8e1c05a5796 100644
--- a/t/unit/tasks/test_tasks.py
+++ b/t/unit/tasks/test_tasks.py
@@ -144,6 +144,27 @@ def retry_task_auto_retry_exception_with_new_args(self, ret=None, place_holder=N
self.retry_task_auto_retry_exception_with_new_args = retry_task_auto_retry_exception_with_new_args
+ @self.app.task(bind=True, max_retries=10, iterations=0, shared=False,
+ autoretry_for=(Exception,))
+ def retry_task_max_retries_override(self, **kwargs):
+ # Test for #6436
+ self.iterations += 1
+ if self.iterations == 3:
+ # I wanna force fail here cause i have enough
+ self.retry(exc=MyCustomException, max_retries=0)
+ self.retry(exc=MyCustomException)
+
+ self.retry_task_max_retries_override = retry_task_max_retries_override
+
+ @self.app.task(bind=True, max_retries=0, iterations=0, shared=False,
+ autoretry_for=(Exception,))
+ def retry_task_explicit_exception(self, **kwargs):
+ # Test for #6436
+ self.iterations += 1
+ raise MyCustomException()
+
+ self.retry_task_explicit_exception = retry_task_explicit_exception
+
@self.app.task(bind=True, max_retries=3, iterations=0, shared=False)
def retry_task_raise_without_throw(self, **kwargs):
self.iterations += 1
@@ -432,6 +453,22 @@ def test_eager_retry_with_new_params(self):
def test_eager_retry_with_autoretry_for_exception(self):
assert self.retry_task_auto_retry_exception_with_new_args.si(place_holder="test").apply().get() == "test"
+ def test_retry_task_max_retries_override(self):
+ self.retry_task_max_retries_override.max_retries = 10
+ self.retry_task_max_retries_override.iterations = 0
+ result = self.retry_task_max_retries_override.apply()
+ with pytest.raises(MyCustomException):
+ result.get()
+ assert self.retry_task_max_retries_override.iterations == 3
+
+ def test_retry_task_explicit_exception(self):
+ self.retry_task_explicit_exception.max_retries = 0
+ self.retry_task_explicit_exception.iterations = 0
+ result = self.retry_task_explicit_exception.apply()
+ with pytest.raises(MyCustomException):
+ result.get()
+ assert self.retry_task_explicit_exception.iterations == 1
+
def test_retry_eager_should_return_value(self):
self.retry_task.max_retries = 3
self.retry_task.iterations = 0
From 8fee0bfeb91fc9483a041bfd169b534a8aa86bf6 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 2 Nov 2020 18:11:03 +0200
Subject: [PATCH 058/415] Happify linter.
---
celery/app/autoretry.py | 6 +++---
celery/exceptions.py | 1 -
t/integration/test_canvas.py | 1 +
t/unit/worker/test_request.py | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/celery/app/autoretry.py b/celery/app/autoretry.py
index 5da8487bd5f..a22b9f04717 100644
--- a/celery/app/autoretry.py
+++ b/celery/app/autoretry.py
@@ -48,9 +48,9 @@ def run(*args, **kwargs):
full_jitter=retry_jitter)
# Override max_retries
if hasattr(task, 'override_max_retries'):
- retry_kwargs['max_retries'] = getattr(task,
- 'override_max_retries',
- task.max_retries)
+ retry_kwargs['max_retries'] = getattr(task,
+ 'override_max_retries',
+ task.max_retries)
ret = task.retry(exc=exc, **retry_kwargs)
# Stop propagation
if hasattr(task, 'override_max_retries'):
diff --git a/celery/exceptions.py b/celery/exceptions.py
index ee903290f2f..768cd4d22d2 100644
--- a/celery/exceptions.py
+++ b/celery/exceptions.py
@@ -293,4 +293,3 @@ def __init__(self, *args, **kwargs):
def __repr__(self):
return super().__repr__() + " state:" + self.state + " task_id:" + self.task_id
-
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 4ae027fb10a..34b8099674c 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -1239,6 +1239,7 @@ class test_signature_serialization:
signature object was not properly deserialized from its dictionary
representation, and would explode later on if it were used as a signature.
"""
+
def test_rebuild_nested_chain_chain(self, manager):
sig = chain(
tasks.return_nested_signature_chain_chain.s(),
diff --git a/t/unit/worker/test_request.py b/t/unit/worker/test_request.py
index 3ed7c553d15..d63ccbb1147 100644
--- a/t/unit/worker/test_request.py
+++ b/t/unit/worker/test_request.py
@@ -1205,7 +1205,7 @@ def test_execute_using_pool_with_none_timelimit_header(self):
def test_execute_using_pool__defaults_of_hybrid_to_proto2(self):
weakref_ref = Mock(name='weakref.ref')
headers = strategy.hybrid_to_proto2(Mock(headers=None), {'id': uuid(),
- 'task': self.mytask.name})[1]
+ 'task': self.mytask.name})[1]
job = self.zRequest(revoked_tasks=set(), ref=weakref_ref, **headers)
job.execute_using_pool(self.pool)
assert job._apply_result
From 6cf4f40fc58fe8585721f4df95a1c77d25106dbf Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 2 Nov 2020 18:11:26 +0200
Subject: [PATCH 059/415] Raise proper error when replacing with an empty
chain. (#6452)
Fixes #6451.
---
celery/app/task.py | 7 ++++++-
t/integration/tasks.py | 5 +++++
t/integration/test_canvas.py | 16 ++++++++++++----
3 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/celery/app/task.py b/celery/app/task.py
index cab270cfa30..2265ebb9e67 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -8,7 +8,7 @@
from celery import current_app, group, states
from celery._state import _task_stack
-from celery.canvas import signature
+from celery.canvas import _chain, signature
from celery.exceptions import (Ignore, ImproperlyConfigured,
MaxRetriesExceededError, Reject, Retry)
from celery.local import class_property
@@ -882,6 +882,11 @@ def replace(self, sig):
link=self.request.callbacks,
link_error=self.request.errbacks,
)
+ elif isinstance(sig, _chain):
+ if not sig.tasks:
+ raise ImproperlyConfigured(
+ "Cannot replace with an empty chain"
+ )
if self.request.chain:
# We need to freeze the new signature with the current task's ID to
diff --git a/t/integration/tasks.py b/t/integration/tasks.py
index 8aa13bc1797..1aaeed32378 100644
--- a/t/integration/tasks.py
+++ b/t/integration/tasks.py
@@ -100,6 +100,11 @@ def replace_with_chain_which_raises(self, *args, link_msg=None):
return self.replace(c)
+@shared_task(bind=True)
+def replace_with_empty_chain(self, *_):
+ return self.replace(chain())
+
+
@shared_task(bind=True)
def add_to_all(self, nums, val):
"""Add the given value to all supplied numbers."""
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 34b8099674c..fe594807ee5 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -6,7 +6,7 @@
from celery import chain, chord, group, signature
from celery.backends.base import BaseKeyValueStoreBackend
-from celery.exceptions import TimeoutError
+from celery.exceptions import ImproperlyConfigured, TimeoutError
from celery.result import AsyncResult, GroupResult, ResultSet
from . import tasks
@@ -15,9 +15,10 @@
add_to_all, add_to_all_to_chord, build_chain_inside_task,
chord_error, collect_ids, delayed_sum,
delayed_sum_with_soft_guard, fail, identity, ids,
- print_unicode, raise_error, redis_echo, retry_once,
- return_exception, return_priority, second_order_replace1,
- tsum, replace_with_chain, replace_with_chain_which_raises)
+ print_unicode, raise_error, redis_echo,
+ replace_with_chain, replace_with_chain_which_raises,
+ replace_with_empty_chain, retry_once, return_exception,
+ return_priority, second_order_replace1, tsum)
RETRYABLE_EXCEPTIONS = (OSError, ConnectionError, TimeoutError)
@@ -584,6 +585,13 @@ def test_chain_with_eb_replaced_with_chain_with_eb(self, manager):
assert redis_connection.blpop('redis-echo', min(1, TIMEOUT)) is None
redis_connection.delete('redis-echo')
+ def test_replace_chain_with_empty_chain(self, manager):
+ r = chain(identity.s(1), replace_with_empty_chain.s()).delay()
+
+ with pytest.raises(ImproperlyConfigured,
+ match="Cannot replace with an empty chain"):
+ r.get(timeout=TIMEOUT)
+
class test_result_set:
From 8bed67cbc85fb1f7ee71e2cd50cd76ec36ea521c Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 2 Nov 2020 19:51:28 +0200
Subject: [PATCH 060/415] Update changelog.
---
Changelog.rst | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/Changelog.rst b/Changelog.rst
index 14c2b0b0b4c..b65686a6708 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -8,6 +8,26 @@ This document contains change notes for bugfix & new features
in the 5.0.x series, please see :ref:`whatsnew-5.0` for
an overview of what's new in Celery 5.0.
+.. _version-5.0.2:
+
+5.0.2
+=====
+:release-date: 2020-11-02 8.00 P.M UTC+2:00
+:release-by: Omer Katz
+
+- Fix _autodiscover_tasks_from_fixups (#6424).
+- Flush worker prints, notably the banner (#6432).
+- **Breaking Change**: Remove `ha_policy` from queue definition. (#6440)
+
+ This argument has no effect since RabbitMQ 3.0.
+ Therefore, We feel comfortable dropping it in a patch release.
+
+- Python 3.9 support (#6418).
+- **Regression**: When using the prefork pool, pick the fair scheduling strategy by default (#6447).
+- Preserve callbacks when replacing a task with a chain (#6189).
+- Fix max_retries override on `self.retry()` (#6436).
+- Raise proper error when replacing with an empty chain (#6452)
+
.. _version-5.0.1:
5.0.1
From f50cf7d9944558167b85c14d73e8f790da251730 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 2 Nov 2020 19:52:00 +0200
Subject: [PATCH 061/415] =?UTF-8?q?Bump=20version:=205.0.1=20=E2=86=92=205?=
=?UTF-8?q?.0.2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.bumpversion.cfg | 2 +-
README.rst | 6 +++---
celery/__init__.py | 2 +-
docs/includes/introduction.txt | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index ea5f7e924c0..7be80a9bab6 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 5.0.1
+current_version = 5.0.2
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z\d]+)?
diff --git a/README.rst b/README.rst
index 25b5e2c8e2b..529669641d9 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
|build-status| |coverage| |license| |wheel| |pyversion| |pyimp| |ocbackerbadge| |ocsponsorbadge|
-:Version: 5.0.1 (singularity)
+:Version: 5.0.2 (singularity)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
@@ -57,7 +57,7 @@ in such a way that the client enqueues an URL to be requested by a worker.
What do I need?
===============
-Celery version 5.0.1 runs on,
+Celery version 5.0.2 runs on,
- Python (3.6, 3.7, 3.8)
- PyPy3.6 (7.6)
@@ -89,7 +89,7 @@ Get Started
===========
If this is the first time you're trying to use Celery, or you're
-new to Celery 5.0.1 coming from previous versions then you should read our
+new to Celery 5.0.2 coming from previous versions then you should read our
getting started tutorials:
- `First steps with Celery`_
diff --git a/celery/__init__.py b/celery/__init__.py
index a9f497130e7..7ed8e28cb0a 100644
--- a/celery/__init__.py
+++ b/celery/__init__.py
@@ -17,7 +17,7 @@
SERIES = 'singularity'
-__version__ = '5.0.1'
+__version__ = '5.0.2'
__author__ = 'Ask Solem'
__contact__ = 'auvipy@gmail.com'
__homepage__ = 'http://celeryproject.org'
diff --git a/docs/includes/introduction.txt b/docs/includes/introduction.txt
index 188fd291478..a19bd2a012a 100644
--- a/docs/includes/introduction.txt
+++ b/docs/includes/introduction.txt
@@ -1,4 +1,4 @@
-:Version: 5.0.1 (cliffs)
+:Version: 5.0.2 (cliffs)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
From 7434545f55a2de4b6c636ca69fac1ede455bc449 Mon Sep 17 00:00:00 2001
From: Maarten Fonville
Date: Mon, 2 Nov 2020 21:44:55 +0100
Subject: [PATCH 062/415] Update daemonizing.rst
Improved systemd documentation for auto-start of the service, and mention the possibility to depend on RabbitMQ service. Also add Restart=always for Celery Beat example
---
docs/userguide/daemonizing.rst | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/docs/userguide/daemonizing.rst b/docs/userguide/daemonizing.rst
index ae804f6c32e..8b74f73bfb4 100644
--- a/docs/userguide/daemonizing.rst
+++ b/docs/userguide/daemonizing.rst
@@ -415,6 +415,12 @@ This is an example systemd file:
Once you've put that file in :file:`/etc/systemd/system`, you should run
:command:`systemctl daemon-reload` in order that Systemd acknowledges that file.
You should also run that command each time you modify it.
+Use :command:`systemctl enable celery.service` if you want the celery service to
+automatically start when (re)booting the system.
+
+Optionally you can specify extra dependencies for the celery service: e.g. if you use
+RabbitMQ as a broker, you could specify ``rabbitmq-server.service`` in both ``After=`` and ``Requires=``
+in the ``[Unit]`` `systemd section `_.
To configure user, group, :command:`chdir` change settings:
``User``, ``Group``, and ``WorkingDirectory`` defined in
@@ -496,10 +502,16 @@ This is an example systemd file for Celery Beat:
ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} beat \
--pidfile=${CELERYBEAT_PID_FILE} \
--logfile=${CELERYBEAT_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
+ Restart=always
[Install]
WantedBy=multi-user.target
+Once you've put that file in :file:`/etc/systemd/system`, you should run
+:command:`systemctl daemon-reload` in order that Systemd acknowledges that file.
+You should also run that command each time you modify it.
+Use :command:`systemctl enable celerybeat.service` if you want the celery beat
+service to automatically start when (re)booting the system.
Running the worker with superuser privileges (root)
======================================================================
From d2a9b74b2122b2af0c5f219bc5800928870bd532 Mon Sep 17 00:00:00 2001
From: Maarten Fonville
Date: Mon, 2 Nov 2020 22:01:03 +0100
Subject: [PATCH 063/415] Update celerybeat.service
---
extra/systemd/celerybeat.service | 1 +
1 file changed, 1 insertion(+)
diff --git a/extra/systemd/celerybeat.service b/extra/systemd/celerybeat.service
index 8cb2ad3687e..c1b2034dcdd 100644
--- a/extra/systemd/celerybeat.service
+++ b/extra/systemd/celerybeat.service
@@ -11,6 +11,7 @@ WorkingDirectory=/opt/celery
ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} beat \
--pidfile=${CELERYBEAT_PID_FILE} \
--logfile=${CELERYBEAT_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
+Restart=always
[Install]
WantedBy=multi-user.target
From 25eb27b210dce6a7771671b77fe6a385ce7d7eaa Mon Sep 17 00:00:00 2001
From: Mathieu Rollet
Date: Tue, 3 Nov 2020 10:58:48 +0100
Subject: [PATCH 064/415] Fix old celery beat variables
Change made 5 days ago in 7c3da03a07882ca86b801ad78dd509a67cba60af is faulty, the correct celery beat variables do start with `CELERYBEAT` and not `CELERY_BEAT`
---
docs/userguide/configuration.rst | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index f942188d07d..0e3b8376fa0 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -65,11 +65,11 @@ have been moved into a new ``task_`` prefix.
``CELERY_IMPORTS`` :setting:`imports`
``CELERY_INCLUDE`` :setting:`include`
``CELERY_TIMEZONE`` :setting:`timezone`
-``CELERY_BEAT_MAX_LOOP_INTERVAL`` :setting:`beat_max_loop_interval`
-``CELERY_BEAT_SCHEDULE`` :setting:`beat_schedule`
-``CELERY_BEAT_SCHEDULER`` :setting:`beat_scheduler`
-``CELERY_BEAT_SCHEDULE_FILENAME`` :setting:`beat_schedule_filename`
-``CELERY_BEAT_SYNC_EVERY`` :setting:`beat_sync_every`
+``CELERYBEAT_MAX_LOOP_INTERVAL`` :setting:`beat_max_loop_interval`
+``CELERYBEAT_SCHEDULE`` :setting:`beat_schedule`
+``CELERYBEAT_SCHEDULER`` :setting:`beat_scheduler`
+``CELERYBEAT_SCHEDULE_FILENAME`` :setting:`beat_schedule_filename`
+``CELERYBEAT_SYNC_EVERY`` :setting:`beat_sync_every`
``BROKER_URL`` :setting:`broker_url`
``BROKER_TRANSPORT`` :setting:`broker_transport`
``BROKER_TRANSPORT_OPTIONS`` :setting:`broker_transport_options`
From 762d2e6e12d56e61274d2ef3d279864e99520dcd Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 3 Nov 2020 14:03:11 +0200
Subject: [PATCH 065/415] Fix formatting.
---
docs/whatsnew-5.0.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/whatsnew-5.0.rst b/docs/whatsnew-5.0.rst
index 9360a5b9588..3f93ce3e979 100644
--- a/docs/whatsnew-5.0.rst
+++ b/docs/whatsnew-5.0.rst
@@ -291,7 +291,7 @@ Starting from Celery 5.0, the pytest plugin is no longer enabled by default.
Please refer to the :ref:`documentation ` for instructions.
Ordered Group Results for the Redis Result Backend
--------------------------------------------------
+--------------------------------------------------
Previously group results were not ordered by their invocation order.
Celery 4.4.7 introduced an opt-in feature to make them ordered.
From db35ca1ccaebff02e1fe36912963803d277b4979 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 3 Nov 2020 14:10:30 +0200
Subject: [PATCH 066/415] Fix formatting.
---
docs/userguide/configuration.rst | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index 0e3b8376fa0..e9c1c76c151 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -65,11 +65,11 @@ have been moved into a new ``task_`` prefix.
``CELERY_IMPORTS`` :setting:`imports`
``CELERY_INCLUDE`` :setting:`include`
``CELERY_TIMEZONE`` :setting:`timezone`
-``CELERYBEAT_MAX_LOOP_INTERVAL`` :setting:`beat_max_loop_interval`
-``CELERYBEAT_SCHEDULE`` :setting:`beat_schedule`
-``CELERYBEAT_SCHEDULER`` :setting:`beat_scheduler`
-``CELERYBEAT_SCHEDULE_FILENAME`` :setting:`beat_schedule_filename`
-``CELERYBEAT_SYNC_EVERY`` :setting:`beat_sync_every`
+``CELERYBEAT_MAX_LOOP_INTERVAL`` :setting:`beat_max_loop_interval`
+``CELERYBEAT_SCHEDULE`` :setting:`beat_schedule`
+``CELERYBEAT_SCHEDULER`` :setting:`beat_scheduler`
+``CELERYBEAT_SCHEDULE_FILENAME`` :setting:`beat_schedule_filename`
+``CELERYBEAT_SYNC_EVERY`` :setting:`beat_sync_every`
``BROKER_URL`` :setting:`broker_url`
``BROKER_TRANSPORT`` :setting:`broker_transport`
``BROKER_TRANSPORT_OPTIONS`` :setting:`broker_transport_options`
From 42361bdd2cb858d24a896d447448b2a6bb47307d Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Wed, 4 Nov 2020 00:31:28 +1100
Subject: [PATCH 067/415] fix: Make `--workdir` eager for early handling
(#6457)
This change makes the `--workdir` options an eager one which `click`
will process early for us, before any of the others. At the same time,
we add a callback which ensures that the `chdir()` is run during
handling of the argument so that all subsequent actions (e.g. app
loading) occur in the specified working directory.
Fixes #6445
---
celery/bin/celery.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/celery/bin/celery.py b/celery/bin/celery.py
index 5488d17c40e..6626c21fa64 100644
--- a/celery/bin/celery.py
+++ b/celery/bin/celery.py
@@ -1,5 +1,6 @@
"""Celery Command Line Interface."""
import os
+import pathlib
import traceback
import click
@@ -94,6 +95,9 @@ def convert(self, value, param, ctx):
help_group="Global Options")
@click.option('--workdir',
cls=CeleryOption,
+ type=pathlib.Path,
+ callback=lambda _, __, wd: os.chdir(wd) if wd else None,
+ is_eager=True,
help_group="Global Options")
@click.option('-C',
'--no-color',
@@ -121,8 +125,6 @@ def celery(ctx, app, broker, result_backend, loader, config, workdir,
click.echo(ctx.get_help())
ctx.exit()
- if workdir:
- os.chdir(workdir)
if loader:
# Default app takes loader from this env (Issue #1066).
os.environ['CELERY_LOADER'] = loader
From 84951b1441ef242c75fe48e2100783b3081487c0 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 3 Nov 2020 16:52:41 +0200
Subject: [PATCH 068/415] Fix example.
Fixes #6459.
---
examples/next-steps/proj/celery.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/next-steps/proj/celery.py b/examples/next-steps/proj/celery.py
index f9be2a1c549..39ce69199a9 100644
--- a/examples/next-steps/proj/celery.py
+++ b/examples/next-steps/proj/celery.py
@@ -2,7 +2,7 @@
app = Celery('proj',
broker='amqp://',
- backend='amqp://',
+ backend='rpc://',
include=['proj.tasks'])
# Optional configuration, see the application user guide.
From 406f04a082949ac42ec7a4af94fed896c515aaa4 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 5 Nov 2020 08:59:10 +0200
Subject: [PATCH 069/415] When using the MongoDB backend, don't cleanup if
result_expires is 0 or None. (#6462)
Fixes #6450.
---
celery/backends/mongodb.py | 3 +++
t/unit/backends/test_mongodb.py | 6 ++++++
2 files changed, 9 insertions(+)
diff --git a/celery/backends/mongodb.py b/celery/backends/mongodb.py
index 5ae3ddf8223..76eab766b75 100644
--- a/celery/backends/mongodb.py
+++ b/celery/backends/mongodb.py
@@ -248,6 +248,9 @@ def _forget(self, task_id):
def cleanup(self):
"""Delete expired meta-data."""
+ if not self.expires:
+ return
+
self.collection.delete_many(
{'date_done': {'$lt': self.app.now() - self.expires_delta}},
)
diff --git a/t/unit/backends/test_mongodb.py b/t/unit/backends/test_mongodb.py
index fb304b7e369..5a391d86d30 100644
--- a/t/unit/backends/test_mongodb.py
+++ b/t/unit/backends/test_mongodb.py
@@ -485,6 +485,12 @@ def test_cleanup(self, mock_get_database):
mock_get_database.assert_called_once_with()
mock_collection.delete_many.assert_called()
+ self.backend.collections = mock_collection = Mock()
+ self.backend.expires = None
+
+ self.backend.cleanup()
+ mock_collection.delete_many.assert_not_called()
+
def test_get_database_authfailure(self):
x = MongoBackend(app=self.app)
x._get_connection = Mock()
From b038786209250bfd77de0732fc344fc204e7e54a Mon Sep 17 00:00:00 2001
From: Mike DePalatis
Date: Sun, 8 Nov 2020 06:53:00 -0700
Subject: [PATCH 070/415] Add missing space (#6468)
---
celery/bin/worker.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/celery/bin/worker.py b/celery/bin/worker.py
index 0472fde4c4b..db1c125a185 100644
--- a/celery/bin/worker.py
+++ b/celery/bin/worker.py
@@ -135,7 +135,7 @@ def detach(path, argv, logfile=None, pidfile=None, uid=None,
type=click.Path(),
callback=lambda ctx, _, value: value or ctx.obj.app.conf.worker_state_db,
help_group="Worker Options",
- help="Path to the state database. The extension '.db' may be"
+ help="Path to the state database. The extension '.db' may be "
"appended to the filename.")
@click.option('-l',
'--loglevel',
From 366264ee00fb0ecb9c8a7cf06438cd9e05da107b Mon Sep 17 00:00:00 2001
From: partizan
Date: Sun, 8 Nov 2020 15:55:22 +0200
Subject: [PATCH 071/415] Fix passing queues into purge command (#6469)
In current wersion calling `celery --app my.celery_app purge -Q queue_name` is failing with following trace:
```
names = (queues or set(app.amqp.queues.keys())) - exclude_queues
TypeError: unsupported operand type(s) for -: 'list' and 'list'
```
Becouse code is expecting set and `queues` is actually a list.
Here is a fix.
---
celery/bin/purge.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/celery/bin/purge.py b/celery/bin/purge.py
index 38245d02ff0..609a9a0f660 100644
--- a/celery/bin/purge.py
+++ b/celery/bin/purge.py
@@ -32,10 +32,10 @@ def purge(ctx, force, queues, exclude_queues):
There's no undo operation for this command.
"""
- queues = queues or set()
- exclude_queues = exclude_queues or set()
app = ctx.obj.app
- names = (queues or set(app.amqp.queues.keys())) - exclude_queues
+ queues = set(queues or app.amqp.queues.keys())
+ exclude_queues = set(exclude_queues or [])
+ names = queues - exclude_queues
qnum = len(names)
if names:
From 65fc5f49a58e9d0da433376c0d80eafaa01c2622 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 3 Nov 2020 15:50:45 +0200
Subject: [PATCH 072/415] Change donations sidebar to direct users to
OpenCollective.
---
docs/_templates/sidebardonations.html | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/docs/_templates/sidebardonations.html b/docs/_templates/sidebardonations.html
index d6e6dfaa788..9049cab2cab 100644
--- a/docs/_templates/sidebardonations.html
+++ b/docs/_templates/sidebardonations.html
@@ -2,12 +2,7 @@
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px">
- Please help support this community project with a donation:
-
+
Donations
+
Please help support this community project with a donation.
+
From 2a6c7cfe3b1283961887bf1cb3f5aa6c8aa70820 Mon Sep 17 00:00:00 2001
From: Nick Pope
Date: Tue, 10 Nov 2020 13:49:04 +0000
Subject: [PATCH 073/415] Added pytest to extras.
Missed in 9a6c2923e859b6993227605610255bd632c1ae68.
---
setup.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/setup.py b/setup.py
index c5843c28321..35f2dd6b084 100644
--- a/setup.py
+++ b/setup.py
@@ -33,6 +33,7 @@
'msgpack',
'pymemcache',
'pyro',
+ 'pytest',
'redis',
's3',
'slmq',
From 28ebcce5d277839011f7782755ac8452b37d6afe Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 17 Nov 2020 09:34:46 +0200
Subject: [PATCH 074/415] Restore app.start() and app.worker_main() (#6481)
* Restore `app.start()` and `app.worker_main()`.
* Update celery/app/base.py
Co-authored-by: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
* Fix spelling error.
Co-authored-by: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
---
celery/app/base.py | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/celery/app/base.py b/celery/app/base.py
index ab9433a8a4e..ed4bd748b56 100644
--- a/celery/app/base.py
+++ b/celery/app/base.py
@@ -1,12 +1,14 @@
"""Actual App instance implementation."""
import inspect
import os
+import sys
import threading
import warnings
from collections import UserDict, defaultdict, deque
from datetime import datetime
from operator import attrgetter
+from click.exceptions import Exit
from kombu import pools
from kombu.clocks import LamportClock
from kombu.common import oid_from
@@ -342,6 +344,30 @@ def close(self):
self._pool = None
_deregister_app(self)
+ def start(self, argv=None):
+ from celery.bin.celery import celery
+
+ celery.params[0].default = self
+
+ try:
+ celery.main(args=argv, standalone_mode=False)
+ except Exit as e:
+ return e.exit_code
+ finally:
+ celery.params[0].default = None
+
+ def worker_main(self, argv=None):
+ if argv is None:
+ argv = sys.argv
+
+ if 'worker' not in argv:
+ raise ValueError(
+ "The worker sub-command must be specified in argv.\n"
+ "Use app.start() to programmatically start other commands."
+ )
+
+ self.start(argv=argv)
+
def task(self, *args, **opts):
"""Decorator to create a task class out of any callable.
From 60ba37900a038420aec0fc76e60c55989f66c718 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Wed, 18 Nov 2020 17:45:23 +1100
Subject: [PATCH 075/415] fix: `node_format()` logfile before detaching
Fixes #6426
---
celery/bin/worker.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/celery/bin/worker.py b/celery/bin/worker.py
index db1c125a185..cd826b89b17 100644
--- a/celery/bin/worker.py
+++ b/celery/bin/worker.py
@@ -94,6 +94,11 @@ def detach(path, argv, logfile=None, pidfile=None, uid=None,
executable=None, hostname=None):
"""Detach program by argv."""
fake = 1 if C_FAKEFORK else fake
+ # `detached()` will attempt to touch the logfile to confirm that error
+ # messages won't be lost after detaching stdout/err, but this means we need
+ # to pre-format it rather than relying on `setup_logging_subsystem()` like
+ # we can elsewhere.
+ logfile = node_format(logfile, hostname)
with detached(logfile, pidfile, uid, gid, umask, workdir, fake,
after_forkers=False):
try:
From e2031688284484d5b5a57ba29cd9cae2d9a81e39 Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Sun, 22 Nov 2020 16:59:14 +0100
Subject: [PATCH 076/415] Multithreaded backend (#6416)
* Cache backend to thread local storage instead of global variable
* Cache oid to thread local storage instead of global variable
* Improve code returning thread_local data
* Move thread local storage to Celery class, introduced thread_oid and added unittests
---
celery/app/base.py | 24 +++++++++++++--
celery/backends/rpc.py | 4 +--
celery/canvas.py | 2 +-
t/unit/app/test_app.py | 59 +++++++++++++++++++++++++++++++++++++
t/unit/backends/test_rpc.py | 17 ++++++++++-
t/unit/tasks/test_chord.py | 7 ++---
t/unit/tasks/test_result.py | 47 ++++++++++++++---------------
t/unit/test_canvas.py | 33 +++++++++++++++++++++
8 files changed, 159 insertions(+), 34 deletions(-)
create mode 100644 t/unit/test_canvas.py
diff --git a/celery/app/base.py b/celery/app/base.py
index ed4bd748b56..27e5b610ca7 100644
--- a/celery/app/base.py
+++ b/celery/app/base.py
@@ -206,6 +206,8 @@ class name.
task_cls = 'celery.app.task:Task'
registry_cls = 'celery.app.registry:TaskRegistry'
+ #: Thread local storage.
+ _local = None
_fixups = None
_pool = None
_conf = None
@@ -229,6 +231,9 @@ def __init__(self, main=None, loader=None, backend=None,
changes=None, config_source=None, fixups=None, task_cls=None,
autofinalize=True, namespace=None, strict_typing=True,
**kwargs):
+
+ self._local = threading.local()
+
self.clock = LamportClock()
self.main = main
self.amqp_cls = amqp or self.amqp_cls
@@ -727,7 +732,7 @@ def send_task(self, name, args=None, kwargs=None, countdown=None,
task_id, name, args, kwargs, countdown, eta, group_id, group_index,
expires, retries, chord,
maybe_list(link), maybe_list(link_error),
- reply_to or self.oid, time_limit, soft_time_limit,
+ reply_to or self.thread_oid, time_limit, soft_time_limit,
self.conf.task_send_sent_event,
root_id, parent_id, shadow, chain,
argsrepr=options.get('argsrepr'),
@@ -1185,15 +1190,28 @@ def oid(self):
# which would not work if each thread has a separate id.
return oid_from(self, threads=False)
+ @property
+ def thread_oid(self):
+ """Per-thread unique identifier for this app."""
+ try:
+ return self._local.oid
+ except AttributeError:
+ self._local.oid = new_oid = oid_from(self, threads=True)
+ return new_oid
+
@cached_property
def amqp(self):
"""AMQP related functionality: :class:`~@amqp`."""
return instantiate(self.amqp_cls, app=self)
- @cached_property
+ @property
def backend(self):
"""Current backend instance."""
- return self._get_backend()
+ try:
+ return self._local.backend
+ except AttributeError:
+ self._local.backend = new_backend = self._get_backend()
+ return new_backend
@property
def conf(self):
diff --git a/celery/backends/rpc.py b/celery/backends/rpc.py
index 9b851db4de8..399c1dc7a20 100644
--- a/celery/backends/rpc.py
+++ b/celery/backends/rpc.py
@@ -338,5 +338,5 @@ def binding(self):
@cached_property
def oid(self):
- # cached here is the app OID: name of queue we receive results on.
- return self.app.oid
+ # cached here is the app thread OID: name of queue we receive results on.
+ return self.app.thread_oid
diff --git a/celery/canvas.py b/celery/canvas.py
index 0279965d2ee..a4de76428dc 100644
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -296,7 +296,7 @@ def freeze(self, _id=None, group_id=None, chord=None,
if parent_id:
opts['parent_id'] = parent_id
if 'reply_to' not in opts:
- opts['reply_to'] = self.app.oid
+ opts['reply_to'] = self.app.thread_oid
if group_id and "group_id" not in opts:
opts['group_id'] = group_id
if chord:
diff --git a/t/unit/app/test_app.py b/t/unit/app/test_app.py
index a533d0cc4d4..2512b16cd4f 100644
--- a/t/unit/app/test_app.py
+++ b/t/unit/app/test_app.py
@@ -2,6 +2,7 @@
import itertools
import os
import ssl
+import uuid
from copy import deepcopy
from datetime import datetime, timedelta
from pickle import dumps, loads
@@ -17,6 +18,7 @@
from celery.app import base as _appbase
from celery.app import defaults
from celery.exceptions import ImproperlyConfigured
+from celery.backends.base import Backend
from celery.loaders.base import unconfigured
from celery.platforms import pyimplementation
from celery.utils.collections import DictAttribute
@@ -987,6 +989,63 @@ class CustomCelery(type(self.app)):
app = CustomCelery(set_as_current=False)
assert isinstance(app.tasks, TaskRegistry)
+ def test_oid(self):
+ # Test that oid is global value.
+ oid1 = self.app.oid
+ oid2 = self.app.oid
+ uuid.UUID(oid1)
+ uuid.UUID(oid2)
+ assert oid1 == oid2
+
+ def test_global_oid(self):
+ # Test that oid is global value also within threads
+ main_oid = self.app.oid
+ uuid.UUID(main_oid)
+ from concurrent.futures import ThreadPoolExecutor
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(lambda: self.app.oid)
+ thread_oid = future.result()
+ uuid.UUID(thread_oid)
+ assert main_oid == thread_oid
+
+ def test_thread_oid(self):
+ # Test that thread_oid is global value in single thread.
+ oid1 = self.app.thread_oid
+ oid2 = self.app.thread_oid
+ uuid.UUID(oid1)
+ uuid.UUID(oid2)
+ assert oid1 == oid2
+
+ def test_backend(self):
+ # Test that app.bakend returns the same backend in single thread
+ backend1 = self.app.backend
+ backend2 = self.app.backend
+ assert isinstance(backend1, Backend)
+ assert isinstance(backend2, Backend)
+ assert backend1 is backend2
+
+ def test_thread_backend(self):
+ # Test that app.bakend returns the new backend for each thread
+ main_backend = self.app.backend
+ from concurrent.futures import ThreadPoolExecutor
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(lambda: self.app.backend)
+ thread_backend = future.result()
+ assert isinstance(main_backend, Backend)
+ assert isinstance(thread_backend, Backend)
+ assert main_backend is not thread_backend
+
+ def test_thread_oid_is_local(self):
+ # Test that thread_oid is local to thread.
+ main_oid = self.app.thread_oid
+ uuid.UUID(main_oid)
+ from concurrent.futures import ThreadPoolExecutor
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(lambda: self.app.thread_oid)
+ thread_oid = future.result()
+ uuid.UUID(thread_oid)
+ assert main_oid != thread_oid
+
class test_defaults:
diff --git a/t/unit/backends/test_rpc.py b/t/unit/backends/test_rpc.py
index f8567400706..71e573da8ff 100644
--- a/t/unit/backends/test_rpc.py
+++ b/t/unit/backends/test_rpc.py
@@ -1,3 +1,4 @@
+import uuid
from unittest.mock import Mock, patch
import pytest
@@ -28,8 +29,22 @@ def setup(self):
def test_oid(self):
oid = self.b.oid
oid2 = self.b.oid
+ assert uuid.UUID(oid)
assert oid == oid2
- assert oid == self.app.oid
+ assert oid == self.app.thread_oid
+
+ def test_oid_threads(self):
+ # Verify that two RPC backends executed in different threads
+ # has different oid.
+ oid = self.b.oid
+ from concurrent.futures import ThreadPoolExecutor
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(lambda: RPCBackend(app=self.app).oid)
+ thread_oid = future.result()
+ assert uuid.UUID(oid)
+ assert uuid.UUID(thread_oid)
+ assert oid == self.app.thread_oid
+ assert thread_oid != oid
def test_interface(self):
self.b.on_reply_declare('task_id')
diff --git a/t/unit/tasks/test_chord.py b/t/unit/tasks/test_chord.py
index e25e2ccc229..bbec557831a 100644
--- a/t/unit/tasks/test_chord.py
+++ b/t/unit/tasks/test_chord.py
@@ -1,5 +1,5 @@
from contextlib import contextmanager
-from unittest.mock import Mock, patch, sentinel
+from unittest.mock import Mock, patch, sentinel, PropertyMock
import pytest
@@ -294,9 +294,8 @@ def adds(self, sig, lazy=False):
return self.add_to_chord(sig, lazy)
self.adds = adds
+ @patch('celery.Celery.backend', new=PropertyMock(name='backend'))
def test_add_to_chord(self):
- self.app.backend = Mock(name='backend')
-
sig = self.add.s(2, 2)
sig.delay = Mock(name='sig.delay')
self.adds.request.group = uuid()
@@ -333,8 +332,8 @@ def test_add_to_chord(self):
class test_Chord_task(ChordCase):
+ @patch('celery.Celery.backend', new=PropertyMock(name='backend'))
def test_run(self):
- self.app.backend = Mock()
self.app.backend.cleanup = Mock()
self.app.backend.cleanup.__name__ = 'cleanup'
Chord = self.app.tasks['celery.chord']
diff --git a/t/unit/tasks/test_result.py b/t/unit/tasks/test_result.py
index e3d06db0f30..d16dc9eae26 100644
--- a/t/unit/tasks/test_result.py
+++ b/t/unit/tasks/test_result.py
@@ -708,19 +708,19 @@ def test_get_nested_without_native_join(self):
]),
]),
])
- ts.app.backend = backend
- vals = ts.get()
- assert vals == [
- '1.1',
- [
- '2.1',
+ with patch('celery.Celery.backend', new=backend):
+ vals = ts.get()
+ assert vals == [
+ '1.1',
[
- '3.1',
- '3.2',
- ]
- ],
- ]
+ '2.1',
+ [
+ '3.1',
+ '3.2',
+ ]
+ ],
+ ]
def test_getitem(self):
subs = [MockAsyncResultSuccess(uuid(), app=self.app),
@@ -771,15 +771,16 @@ def test_join_native(self):
results = [self.app.AsyncResult(uuid(), backend=backend)
for i in range(10)]
ts = self.app.GroupResult(uuid(), results)
- ts.app.backend = backend
- backend.ids = [result.id for result in results]
- res = ts.join_native()
- assert res == list(range(10))
- callback = Mock(name='callback')
- assert not ts.join_native(callback=callback)
- callback.assert_has_calls([
- call(r.id, i) for i, r in enumerate(ts.results)
- ])
+
+ with patch('celery.Celery.backend', new=backend):
+ backend.ids = [result.id for result in results]
+ res = ts.join_native()
+ assert res == list(range(10))
+ callback = Mock(name='callback')
+ assert not ts.join_native(callback=callback)
+ callback.assert_has_calls([
+ call(r.id, i) for i, r in enumerate(ts.results)
+ ])
def test_join_native_raises(self):
ts = self.app.GroupResult(uuid(), [self.app.AsyncResult(uuid())])
@@ -813,9 +814,9 @@ def test_iter_native(self):
results = [self.app.AsyncResult(uuid(), backend=backend)
for i in range(10)]
ts = self.app.GroupResult(uuid(), results)
- ts.app.backend = backend
- backend.ids = [result.id for result in results]
- assert len(list(ts.iter_native())) == 10
+ with patch('celery.Celery.backend', new=backend):
+ backend.ids = [result.id for result in results]
+ assert len(list(ts.iter_native())) == 10
def test_join_timeout(self):
ar = MockAsyncResultSuccess(uuid(), app=self.app)
diff --git a/t/unit/test_canvas.py b/t/unit/test_canvas.py
new file mode 100644
index 00000000000..4ba7ba59f3e
--- /dev/null
+++ b/t/unit/test_canvas.py
@@ -0,0 +1,33 @@
+import uuid
+
+
+class test_Canvas:
+
+ def test_freeze_reply_to(self):
+ # Tests that Canvas.freeze() correctly
+ # creates reply_to option
+
+ @self.app.task
+ def test_task(a, b):
+ return
+
+ s = test_task.s(2, 2)
+ s.freeze()
+
+ from concurrent.futures import ThreadPoolExecutor
+
+ def foo():
+ s = test_task.s(2, 2)
+ s.freeze()
+ return self.app.thread_oid, s.options['reply_to']
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(foo)
+ t_reply_to_app, t_reply_to_opt = future.result()
+
+ assert uuid.UUID(s.options['reply_to'])
+ assert uuid.UUID(t_reply_to_opt)
+ # reply_to must be equal to thread_oid of Application
+ assert self.app.thread_oid == s.options['reply_to']
+ assert t_reply_to_app == t_reply_to_opt
+ # reply_to must be thread-relative.
+ assert t_reply_to_opt != s.options['reply_to']
From dea0bd1672cf8d0017f4dae3dfc216278637f90a Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Tue, 24 Nov 2020 00:09:53 +0100
Subject: [PATCH 077/415] Remove python2 compatibility code
---
celery/app/trace.py | 20 +++++++---------
celery/backends/base.py | 20 +++++++---------
celery/backends/cassandra.py | 9 +++----
celery/concurrency/asynpool.py | 9 +------
celery/concurrency/thread.py | 6 -----
celery/local.py | 2 --
celery/platforms.py | 19 ---------------
celery/utils/collections.py | 42 +++++----------------------------
celery/utils/imports.py | 23 +++++++-----------
t/unit/app/test_log.py | 6 +----
t/unit/backends/test_base.py | 23 +++++-------------
t/unit/backends/test_mongodb.py | 3 ---
t/unit/tasks/test_trace.py | 39 ++++++++++++------------------
t/unit/utils/test_local.py | 3 ---
t/unit/worker/test_request.py | 27 +--------------------
15 files changed, 57 insertions(+), 194 deletions(-)
diff --git a/celery/app/trace.py b/celery/app/trace.py
index bb928f2f20b..f9b8c83e6e6 100644
--- a/celery/app/trace.py
+++ b/celery/app/trace.py
@@ -266,18 +266,14 @@ def traceback_clear(exc=None):
else:
_, _, tb = sys.exc_info()
- if sys.version_info >= (3, 5, 0):
- while tb is not None:
- try:
- tb.tb_frame.clear()
- tb.tb_frame.f_locals
- except RuntimeError:
- # Ignore the exception raised if the frame is still executing.
- pass
- tb = tb.tb_next
-
- elif (2, 7, 0) <= sys.version_info < (3, 0, 0):
- sys.exc_clear()
+ while tb is not None:
+ try:
+ tb.tb_frame.clear()
+ tb.tb_frame.f_locals
+ except RuntimeError:
+ # Ignore the exception raised if the frame is still executing.
+ pass
+ tb = tb.tb_next
def build_tracer(name, task, loader=None, hostname=None, store_errors=True,
diff --git a/celery/backends/base.py b/celery/backends/base.py
index 74fce23c3c4..1aac2a0fc95 100644
--- a/celery/backends/base.py
+++ b/celery/backends/base.py
@@ -261,18 +261,14 @@ def fail_from_current_stack(self, task_id, exc=None):
self.mark_as_failure(task_id, exc, exception_info.traceback)
return exception_info
finally:
- if sys.version_info >= (3, 5, 0):
- while tb is not None:
- try:
- tb.tb_frame.clear()
- tb.tb_frame.f_locals
- except RuntimeError:
- # Ignore the exception raised if the frame is still executing.
- pass
- tb = tb.tb_next
-
- elif (2, 7, 0) <= sys.version_info < (3, 0, 0):
- sys.exc_clear()
+ while tb is not None:
+ try:
+ tb.tb_frame.clear()
+ tb.tb_frame.f_locals
+ except RuntimeError:
+ # Ignore the exception raised if the frame is still executing.
+ pass
+ tb = tb.tb_next
del tb
diff --git a/celery/backends/cassandra.py b/celery/backends/cassandra.py
index 72bb33dfe9f..1220063b63c 100644
--- a/celery/backends/cassandra.py
+++ b/celery/backends/cassandra.py
@@ -1,5 +1,4 @@
"""Apache Cassandra result store backend using the DataStax driver."""
-import sys
import threading
from celery import states
@@ -60,11 +59,9 @@
USING TTL {0}
"""
-if sys.version_info[0] == 3:
- def buf_t(x):
- return bytes(x, 'utf8')
-else:
- buf_t = buffer # noqa
+
+def buf_t(x):
+ return bytes(x, 'utf8')
class CassandraBackend(BaseBackend):
diff --git a/celery/concurrency/asynpool.py b/celery/concurrency/asynpool.py
index 7ea3eb204c9..5f17f247d62 100644
--- a/celery/concurrency/asynpool.py
+++ b/celery/concurrency/asynpool.py
@@ -16,7 +16,6 @@
import gc
import os
import select
-import sys
import time
from collections import Counter, deque, namedtuple
from io import BytesIO
@@ -24,6 +23,7 @@
from pickle import HIGHEST_PROTOCOL
from time import sleep
from weakref import WeakValueDictionary, ref
+from struct import pack, unpack, unpack_from
from billiard import pool as _pool
from billiard.compat import buf_t, isblocking, setblocking
@@ -35,7 +35,6 @@
from kombu.utils.functional import fxrange
from vine import promise
-from celery.platforms import pack, unpack, unpack_from
from celery.utils.functional import noop
from celery.utils.log import get_logger
from celery.worker import state as worker_state
@@ -47,12 +46,6 @@
from _billiard import read as __read__
readcanbuf = True
- # unpack_from supports memoryview in 2.7.6 and 3.3+
- if sys.version_info[0] == 2 and sys.version_info < (2, 7, 6):
-
- def unpack_from(fmt, view, _unpack_from=unpack_from): # noqa
- return _unpack_from(fmt, view.tobytes()) # <- memoryview
-
except ImportError: # pragma: no cover
def __read__(fd, buf, size, read=os.read): # noqa
diff --git a/celery/concurrency/thread.py b/celery/concurrency/thread.py
index eb9c8683c7d..ffd2e507f11 100644
--- a/celery/concurrency/thread.py
+++ b/celery/concurrency/thread.py
@@ -1,6 +1,5 @@
"""Thread execution pool."""
-import sys
from concurrent.futures import ThreadPoolExecutor, wait
from .base import BasePool, apply_target
@@ -25,11 +24,6 @@ class TaskPool(BasePool):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-
- # from 3.5, it is calculated from number of CPUs
- if (3, 0) <= sys.version_info < (3, 5) and self.limit is None:
- self.limit = 5
-
self.executor = ThreadPoolExecutor(max_workers=self.limit)
def on_stop(self):
diff --git a/celery/local.py b/celery/local.py
index 5fc32148ac1..f3803f40bec 100644
--- a/celery/local.py
+++ b/celery/local.py
@@ -539,8 +539,6 @@ def recreate_module(name, compat_modules=None, by_module=None, direct=None,
operator.add,
[tuple(v) for v in [compat_modules, origins, direct, attrs]],
)))
- if sys.version_info[0] < 3:
- _all = [s.encode() for s in _all]
cattrs = {
'_compat_modules': compat_modules,
'_all_by_module': by_module, '_direct': direct,
diff --git a/celery/platforms.py b/celery/platforms.py
index ebda45c49ca..452435be6ac 100644
--- a/celery/platforms.py
+++ b/celery/platforms.py
@@ -11,7 +11,6 @@
import os
import platform as _platform
import signal as _signal
-import struct
import sys
import warnings
from collections import namedtuple
@@ -797,21 +796,3 @@ def check_privileges(accept_content):
warnings.warn(RuntimeWarning(ROOT_DISCOURAGED.format(
uid=uid, euid=euid, gid=gid, egid=egid,
)))
-
-
-if sys.version_info < (2, 7, 7): # pragma: no cover
- import functools
-
- def _to_bytes_arg(fun):
- @functools.wraps(fun)
- def _inner(s, *args, **kwargs):
- return fun(s.encode(), *args, **kwargs)
- return _inner
-
- pack = _to_bytes_arg(struct.pack)
- unpack = _to_bytes_arg(struct.unpack)
- unpack_from = _to_bytes_arg(struct.unpack_from)
-else:
- pack = struct.pack
- unpack = struct.unpack
- unpack_from = struct.unpack_from
diff --git a/celery/utils/collections.py b/celery/utils/collections.py
index b9dbf826fa3..b15e122b6b7 100644
--- a/celery/utils/collections.py
+++ b/celery/utils/collections.py
@@ -1,5 +1,4 @@
"""Custom maps, sets, sequences, and other data structures."""
-import sys
import time
from collections import OrderedDict as _OrderedDict
from collections import deque
@@ -193,24 +192,9 @@ def _iterate_values(self):
yield getattr(self.obj, key)
itervalues = _iterate_values
- if sys.version_info[0] == 3: # pragma: no cover
- items = _iterate_items
- keys = _iterate_keys
- values = _iterate_values
- else:
-
- def keys(self):
- # type: () -> List[Any]
- return list(self)
-
- def items(self):
- # type: () -> List[Tuple[Any, Any]]
- return list(self._iterate_items())
-
- def values(self):
- # type: () -> List[Any]
- return list(self._iterate_values())
-
+ items = _iterate_items
+ keys = _iterate_keys
+ values = _iterate_values
MutableMapping.register(DictAttribute) # noqa: E305
@@ -360,23 +344,9 @@ def _iterate_values(self):
def bind_to(self, callback):
self._observers.append(callback)
- if sys.version_info[0] == 3: # pragma: no cover
- keys = _iterate_keys
- items = _iterate_items
- values = _iterate_values
-
- else: # noqa
- def keys(self):
- # type: () -> List[Any]
- return list(self._iterate_keys())
-
- def items(self):
- # type: () -> List[Tuple[Any, Any]]
- return list(self._iterate_items())
-
- def values(self):
- # type: () -> List[Any]
- return list(self._iterate_values())
+ keys = _iterate_keys
+ items = _iterate_items
+ values = _iterate_values
class ConfigurationView(ChainMap, AttributeDictMixin):
diff --git a/celery/utils/imports.py b/celery/utils/imports.py
index fd9009c32ac..0303bd3c051 100644
--- a/celery/utils/imports.py
+++ b/celery/utils/imports.py
@@ -25,21 +25,14 @@ class NotAPackage(Exception):
"""Raised when importing a package, but it's not a package."""
-if sys.version_info > (3, 3): # pragma: no cover
- def qualname(obj):
- """Return object name."""
- if not hasattr(obj, '__name__') and hasattr(obj, '__class__'):
- obj = obj.__class__
- q = getattr(obj, '__qualname__', None)
- if '.' not in q:
- q = '.'.join((obj.__module__, q))
- return q
-else:
- def qualname(obj): # noqa
- """Return object name."""
- if not hasattr(obj, '__name__') and hasattr(obj, '__class__'):
- obj = obj.__class__
- return '.'.join((obj.__module__, obj.__name__))
+def qualname(obj):
+ """Return object name."""
+ if not hasattr(obj, '__name__') and hasattr(obj, '__class__'):
+ obj = obj.__class__
+ q = getattr(obj, '__qualname__', None)
+ if '.' not in q:
+ q = '.'.join((obj.__module__, q))
+ return q
def instantiate(name, *args, **kwargs):
diff --git a/t/unit/app/test_log.py b/t/unit/app/test_log.py
index 453c3f26702..3793b7e8276 100644
--- a/t/unit/app/test_log.py
+++ b/t/unit/app/test_log.py
@@ -103,8 +103,6 @@ def test_formatException_bytes(self, safe_str, fe):
raise Exception()
except Exception:
assert x.formatException(sys.exc_info())
- if sys.version_info[0] == 2:
- safe_str.assert_called()
@patch('logging.Formatter.format')
def test_format_object(self, _format):
@@ -222,9 +220,7 @@ def test_setup_logger_no_handlers_stream(self):
@patch('os.fstat')
def test_setup_logger_no_handlers_file(self, *args):
tempfile = mktemp(suffix='unittest', prefix='celery')
- _open = ('builtins.open' if sys.version_info[0] == 3
- else '__builtin__.open')
- with patch(_open) as osopen:
+ with patch('builtins.open') as osopen:
with mock.restore_logging():
files = defaultdict(StringIO)
diff --git a/t/unit/backends/test_base.py b/t/unit/backends/test_base.py
index fbcda1ceb3e..0e4bb133c85 100644
--- a/t/unit/backends/test_base.py
+++ b/t/unit/backends/test_base.py
@@ -1,4 +1,3 @@
-import sys
from contextlib import contextmanager
from unittest.mock import ANY, Mock, call, patch, sentinel
@@ -258,7 +257,6 @@ def test_json_exception_arguments(self):
y = self.b.exception_to_python(x)
assert isinstance(y, Exception)
- @pytest.mark.skipif(sys.version_info < (3, 3), reason='no qualname support')
def test_json_exception_nested(self):
self.b.serializer = 'json'
x = self.b.prepare_exception(objectexception.Nested('msg'))
@@ -276,10 +274,7 @@ def test_impossible(self):
assert str(x)
y = self.b.exception_to_python(x)
assert y.__class__.__name__ == 'Impossible'
- if sys.version_info < (2, 5):
- assert y.__class__.__module__
- else:
- assert y.__class__.__module__ == 'foo.module'
+ assert y.__class__.__module__ == 'foo.module'
def test_regular(self):
self.b.serializer = 'pickle'
@@ -403,9 +398,6 @@ def test_fail_from_current_stack(self):
self.b.mark_as_failure = Mock()
frame_list = []
- if (2, 7, 0) <= sys.version_info < (3, 0, 0):
- sys.exc_clear = Mock()
-
def raise_dummy():
frame_str_temp = str(inspect.currentframe().__repr__)
frame_list.append(frame_str_temp)
@@ -420,14 +412,11 @@ def raise_dummy():
assert args[1] is exc
assert args[2]
- if sys.version_info >= (3, 5, 0):
- tb_ = exc.__traceback__
- while tb_ is not None:
- if str(tb_.tb_frame.__repr__) == frame_list[0]:
- assert len(tb_.tb_frame.f_locals) == 0
- tb_ = tb_.tb_next
- elif (2, 7, 0) <= sys.version_info < (3, 0, 0):
- sys.exc_clear.assert_called()
+ tb_ = exc.__traceback__
+ while tb_ is not None:
+ if str(tb_.tb_frame.__repr__) == frame_list[0]:
+ assert len(tb_.tb_frame.f_locals) == 0
+ tb_ = tb_.tb_next
def test_prepare_value_serializes_group_result(self):
self.b.serializer = 'json'
diff --git a/t/unit/backends/test_mongodb.py b/t/unit/backends/test_mongodb.py
index 5a391d86d30..d0e651ed37c 100644
--- a/t/unit/backends/test_mongodb.py
+++ b/t/unit/backends/test_mongodb.py
@@ -1,5 +1,4 @@
import datetime
-import sys
from pickle import dumps, loads
from unittest.mock import ANY, MagicMock, Mock, patch, sentinel
@@ -659,8 +658,6 @@ def test_encode_success_results(self, mongo_backend_factory, serializer,
backend = mongo_backend_factory(serializer=serializer)
backend.store_result(TASK_ID, result, 'SUCCESS')
recovered = backend.get_result(TASK_ID)
- if sys.version_info.major == 2 and isinstance(recovered, str):
- result_type = str # workaround for python 2 compatibility and `unicode_literals`
assert type(recovered) == result_type
assert recovered == result
diff --git a/t/unit/tasks/test_trace.py b/t/unit/tasks/test_trace.py
index e78b6aa4148..3d7061acea5 100644
--- a/t/unit/tasks/test_trace.py
+++ b/t/unit/tasks/test_trace.py
@@ -176,42 +176,33 @@ def raise_dummy():
except KeyError as exc:
traceback_clear(exc)
- if sys.version_info >= (3, 5, 0):
- tb_ = exc.__traceback__
- while tb_ is not None:
- if str(tb_.tb_frame.__repr__) == frame_list[0]:
- assert len(tb_.tb_frame.f_locals) == 0
- tb_ = tb_.tb_next
- elif (2, 7, 0) <= sys.version_info < (3, 0, 0):
- sys.exc_clear.assert_called()
+ tb_ = exc.__traceback__
+ while tb_ is not None:
+ if str(tb_.tb_frame.__repr__) == frame_list[0]:
+ assert len(tb_.tb_frame.f_locals) == 0
+ tb_ = tb_.tb_next
try:
raise_dummy()
except KeyError as exc:
traceback_clear()
- if sys.version_info >= (3, 5, 0):
- tb_ = exc.__traceback__
- while tb_ is not None:
- if str(tb_.tb_frame.__repr__) == frame_list[0]:
- assert len(tb_.tb_frame.f_locals) == 0
- tb_ = tb_.tb_next
- elif (2, 7, 0) <= sys.version_info < (3, 0, 0):
- sys.exc_clear.assert_called()
+ tb_ = exc.__traceback__
+ while tb_ is not None:
+ if str(tb_.tb_frame.__repr__) == frame_list[0]:
+ assert len(tb_.tb_frame.f_locals) == 0
+ tb_ = tb_.tb_next
try:
raise_dummy()
except KeyError as exc:
traceback_clear(str(exc))
- if sys.version_info >= (3, 5, 0):
- tb_ = exc.__traceback__
- while tb_ is not None:
- if str(tb_.tb_frame.__repr__) == frame_list[0]:
- assert len(tb_.tb_frame.f_locals) == 0
- tb_ = tb_.tb_next
- elif (2, 7, 0) <= sys.version_info < (3, 0, 0):
- sys.exc_clear.assert_called()
+ tb_ = exc.__traceback__
+ while tb_ is not None:
+ if str(tb_.tb_frame.__repr__) == frame_list[0]:
+ assert len(tb_.tb_frame.f_locals) == 0
+ tb_ = tb_.tb_next
@patch('celery.app.trace.traceback_clear')
def test_when_Ignore(self, mock_traceback_clear):
diff --git a/t/unit/utils/test_local.py b/t/unit/utils/test_local.py
index a10accf086d..621a77595b2 100644
--- a/t/unit/utils/test_local.py
+++ b/t/unit/utils/test_local.py
@@ -1,4 +1,3 @@
-import sys
from unittest.mock import Mock
import pytest
@@ -143,8 +142,6 @@ def test_listproxy(self):
x[0:2] = [1, 2]
del(x[0:2])
assert str(x)
- if sys.version_info[0] < 3:
- assert x.__cmp__(object()) == -1
def test_complex_cast(self):
diff --git a/t/unit/worker/test_request.py b/t/unit/worker/test_request.py
index d63ccbb1147..c0d0119d9b8 100644
--- a/t/unit/worker/test_request.py
+++ b/t/unit/worker/test_request.py
@@ -2,15 +2,13 @@
import os
import signal
import socket
-import sys
from datetime import datetime, timedelta
from time import monotonic, time
from unittest.mock import Mock, patch
import pytest
from billiard.einfo import ExceptionInfo
-from kombu.utils.encoding import (default_encode, from_utf8, safe_repr,
- safe_str)
+from kombu.utils.encoding import from_utf8, safe_repr, safe_str
from kombu.utils.uuid import uuid
from celery import states
@@ -99,29 +97,6 @@ def jail(app, task_id, name, args, kwargs):
).retval
-@pytest.mark.skipif(sys.version_info[0] > 3, reason='Py2 only')
-class test_default_encode:
-
- def test_jython(self):
- prev, sys.platform = sys.platform, 'java 1.6.1'
- try:
- assert default_encode(b'foo') == b'foo'
- finally:
- sys.platform = prev
-
- def test_cpython(self):
- prev, sys.platform = sys.platform, 'darwin'
- gfe, sys.getfilesystemencoding = (
- sys.getfilesystemencoding,
- lambda: 'utf-8',
- )
- try:
- assert default_encode(b'foo') == b'foo'
- finally:
- sys.platform = prev
- sys.getfilesystemencoding = gfe
-
-
class test_Retry:
def test_retry_semipredicate(self):
From 2cc4d999106f573802c14a15a22fac6dfd8e781e Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 25 Nov 2020 16:02:48 +0200
Subject: [PATCH 078/415] Restore ability to extend the CLI with new
sub-commands.
---
celery/bin/celery.py | 3 +++
requirements/default.txt | 1 +
2 files changed, 4 insertions(+)
diff --git a/celery/bin/celery.py b/celery/bin/celery.py
index 6626c21fa64..095766c0f4d 100644
--- a/celery/bin/celery.py
+++ b/celery/bin/celery.py
@@ -7,6 +7,8 @@
import click.exceptions
from click.types import ParamType
from click_didyoumean import DYMGroup
+from click_plugins import with_plugins
+from pkg_resources import iter_entry_points
from celery import VERSION_BANNER
from celery.app.utils import find_app
@@ -69,6 +71,7 @@ def convert(self, value, param, ctx):
APP = App()
+@with_plugins(iter_entry_points('celery.commands'))
@click.group(cls=DYMGroup, invoke_without_command=True)
@click.option('-A',
'--app',
diff --git a/requirements/default.txt b/requirements/default.txt
index 124c56679da..3eafbb470f5 100644
--- a/requirements/default.txt
+++ b/requirements/default.txt
@@ -5,3 +5,4 @@ vine>=5.0.0,<6.0
click>=7.0
click-didyoumean>=0.0.3
click-repl>=0.1.6
+click-plugins>=1.1.1
From 07000d826573a97ff633b688bda7bf30db114dfe Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 25 Nov 2020 16:56:42 +0200
Subject: [PATCH 079/415] Adjust documentation to demonstrate how to introduce
sub-command plugins in 5.x.
Fixes #6439.
---
docs/userguide/extending.rst | 39 +++++++++++++-----------------------
1 file changed, 14 insertions(+), 25 deletions(-)
diff --git a/docs/userguide/extending.rst b/docs/userguide/extending.rst
index 969eb72a51c..21ff68ecd2a 100644
--- a/docs/userguide/extending.rst
+++ b/docs/userguide/extending.rst
@@ -816,12 +816,10 @@ Entry-points is special meta-data that can be added to your packages ``setup.py`
and then after installation, read from the system using the :mod:`pkg_resources` module.
Celery recognizes ``celery.commands`` entry-points to install additional
-sub-commands, where the value of the entry-point must point to a valid subclass
-of :class:`celery.bin.base.Command`. There's limited documentation,
-unfortunately, but you can find inspiration from the various commands in the
-:mod:`celery.bin` package.
+sub-commands, where the value of the entry-point must point to a valid click
+command.
-This is how the :pypi:`Flower` monitoring extension adds the :program:`celery flower` command,
+This is how the :pypi:`Flower` monitoring extension may add the :program:`celery flower` command,
by adding an entry-point in :file:`setup.py`:
.. code-block:: python
@@ -830,44 +828,35 @@ by adding an entry-point in :file:`setup.py`:
name='flower',
entry_points={
'celery.commands': [
- 'flower = flower.command:FlowerCommand',
+ 'flower = flower.command:flower',
],
}
)
The command definition is in two parts separated by the equal sign, where the
first part is the name of the sub-command (flower), then the second part is
-the fully qualified symbol path to the class that implements the command:
+the fully qualified symbol path to the function that implements the command:
.. code-block:: text
- flower.command:FlowerCommand
+ flower.command:flower
The module path and the name of the attribute should be separated by colon
as above.
-In the module :file:`flower/command.py`, the command class is defined
-something like this:
+In the module :file:`flower/command.py`, the command function may be defined
+as the following:
.. code-block:: python
- from celery.bin.base import Command
+ import click
-
- class FlowerCommand(Command):
-
- def add_arguments(self, parser):
- parser.add_argument(
- '--port', default=8888, type='int',
- help='Webserver port',
- ),
- parser.add_argument(
- '--debug', action='store_true',
- )
-
- def run(self, port=None, debug=False, **kwargs):
- print('Running our command')
+ @click.command()
+ @click.option('--port', default=8888, type=int, help='Webserver port')
+ @click.option('--debug', is_flag=True)
+ def flower(port, debug):
+ print('Running our command')
Worker API
From 681e72edb918c8ff315665a6abbfc6dd99f303e2 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 26 Nov 2020 11:41:40 +0200
Subject: [PATCH 080/415] autopep8 & isort.
---
celery/concurrency/asynpool.py | 2 +-
celery/utils/collections.py | 1 +
t/unit/app/test_app.py | 2 +-
t/unit/tasks/test_chord.py | 2 +-
4 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/celery/concurrency/asynpool.py b/celery/concurrency/asynpool.py
index 5f17f247d62..f4d1c475a8e 100644
--- a/celery/concurrency/asynpool.py
+++ b/celery/concurrency/asynpool.py
@@ -21,9 +21,9 @@
from io import BytesIO
from numbers import Integral
from pickle import HIGHEST_PROTOCOL
+from struct import pack, unpack, unpack_from
from time import sleep
from weakref import WeakValueDictionary, ref
-from struct import pack, unpack, unpack_from
from billiard import pool as _pool
from billiard.compat import buf_t, isblocking, setblocking
diff --git a/celery/utils/collections.py b/celery/utils/collections.py
index b15e122b6b7..f19014c2dca 100644
--- a/celery/utils/collections.py
+++ b/celery/utils/collections.py
@@ -196,6 +196,7 @@ def _iterate_values(self):
keys = _iterate_keys
values = _iterate_values
+
MutableMapping.register(DictAttribute) # noqa: E305
diff --git a/t/unit/app/test_app.py b/t/unit/app/test_app.py
index 2512b16cd4f..5178cbdf59b 100644
--- a/t/unit/app/test_app.py
+++ b/t/unit/app/test_app.py
@@ -17,8 +17,8 @@
from celery import current_app, shared_task
from celery.app import base as _appbase
from celery.app import defaults
-from celery.exceptions import ImproperlyConfigured
from celery.backends.base import Backend
+from celery.exceptions import ImproperlyConfigured
from celery.loaders.base import unconfigured
from celery.platforms import pyimplementation
from celery.utils.collections import DictAttribute
diff --git a/t/unit/tasks/test_chord.py b/t/unit/tasks/test_chord.py
index bbec557831a..f4e03a0e130 100644
--- a/t/unit/tasks/test_chord.py
+++ b/t/unit/tasks/test_chord.py
@@ -1,5 +1,5 @@
from contextlib import contextmanager
-from unittest.mock import Mock, patch, sentinel, PropertyMock
+from unittest.mock import Mock, PropertyMock, patch, sentinel
import pytest
From ffacfe3e384554d1eeaaeb84a4b8e45171122b18 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 26 Nov 2020 11:55:53 +0200
Subject: [PATCH 081/415] Linters now run using Python 3.9.
---
.travis.yml | 2 +-
tox.ini | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 3c532ee95de..316d206de11 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -59,7 +59,7 @@ jobs:
env: MATRIX_TOXENV=integration-elasticsearch
stage: integration
- - python: '3.8'
+ - python: '3.9'
env:
- TOXENV=flake8,apicheck,configcheck,bandit
- CELERY_TOX_PARALLEL='--parallel --parallel-live'
diff --git a/tox.ini b/tox.ini
index 8ec20b7a007..efdfa1c56be 100644
--- a/tox.ini
+++ b/tox.ini
@@ -65,8 +65,7 @@ basepython =
3.8: python3.8
3.9: python3.9
pypy3: pypy3
- flake8,apicheck,linkcheck,configcheck,bandit: python3.8
- flakeplus: python2.7
+ flake8,apicheck,linkcheck,configcheck,bandit: python3.9
usedevelop = True
[testenv:apicheck]
From 6d4b6cbb61bf19695f1a64774d4e67368a7a6af7 Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Fri, 27 Nov 2020 16:46:26 +0100
Subject: [PATCH 082/415] Fix apply_async() in Calling Tasks userguide
---
docs/userguide/calling.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/userguide/calling.rst b/docs/userguide/calling.rst
index 811820b44a1..363e8f2a9a8 100644
--- a/docs/userguide/calling.rst
+++ b/docs/userguide/calling.rst
@@ -711,13 +711,13 @@ setting or by using the ``ignore_result`` option:
.. code-block:: pycon
- >>> result = add.apply_async(1, 2, ignore_result=True)
+ >>> result = add.apply_async((1, 2), ignore_result=True)
>>> result.get()
None
>>> # Do not ignore result (default)
...
- >>> result = add.apply_async(1, 2, ignore_result=False)
+ >>> result = add.apply_async((1, 2), ignore_result=False)
>>> result.get()
3
From 5529c33ed14520341d3ea7929e2722a7066e7509 Mon Sep 17 00:00:00 2001
From: henribru <6639509+henribru@users.noreply.github.com>
Date: Sun, 29 Nov 2020 15:31:17 +0100
Subject: [PATCH 083/415] Fix dead links in contributing guide (#6506)
---
CONTRIBUTING.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 9814b9c7ee4..e869a4f45fe 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -40,7 +40,7 @@ The Code of Conduct is heavily based on the `Ubuntu Code of Conduct`_, and
the `Pylons Code of Conduct`_.
.. _`Ubuntu Code of Conduct`: https://www.ubuntu.com/community/conduct
-.. _`Pylons Code of Conduct`: http://docs.pylonshq.com/community/conduct.html
+.. _`Pylons Code of Conduct`: https://pylonsproject.org/community-code-of-conduct.html
Be considerate
--------------
@@ -447,7 +447,7 @@ fetch and checkout a remote branch like this::
.. _`Fork a Repo`: https://help.github.com/fork-a-repo/
.. _`Rebasing merge commits in git`:
- https://notes.envato.com/developers/rebasing-merge-commits-in-git/
+ https://web.archive.org/web/20150627054345/http://marketblog.envato.com/general/rebasing-merge-commits-in-git/
.. _`Rebase`: https://help.github.com/rebase/
.. _contributing-docker-development:
From 443ef65248fa2f4cd0931119ce4d5942aa7b2b4b Mon Sep 17 00:00:00 2001
From: henribru <6639509+henribru@users.noreply.github.com>
Date: Mon, 30 Nov 2020 04:17:33 +0100
Subject: [PATCH 084/415] Fix inconsistency in documentation for `link_error`
(#6505)
* Make documentation of link_error consistent
Fixes #4099
* Fix undefined variable in example
* Add to contributors list
---
CONTRIBUTORS.txt | 1 +
docs/userguide/calling.rst | 15 +++++----------
docs/userguide/canvas.rst | 2 +-
3 files changed, 7 insertions(+), 11 deletions(-)
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index a29157e1e57..2e27e625d43 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -278,3 +278,4 @@ Dipankar Achinta, 2019/10/24
Sardorbek Imomaliev, 2020/01/24
Maksym Shalenyi, 2020/07/30
Frazer McLean, 2020/09/29
+Henrik Bruåsdal, 2020/11/29
diff --git a/docs/userguide/calling.rst b/docs/userguide/calling.rst
index 363e8f2a9a8..efeb1bb6c13 100644
--- a/docs/userguide/calling.rst
+++ b/docs/userguide/calling.rst
@@ -135,23 +135,18 @@ task that adds 16 to the previous result, forming the expression
You can also cause a callback to be applied if task raises an exception
-(*errback*), but this behaves differently from a regular callback
-in that it will be passed the id of the parent task, not the result.
-This is because it may not always be possible to serialize
-the exception raised, and so this way the error callback requires
-a result backend to be enabled, and the task must retrieve the result
-of the task instead.
+(*errback*). The worker won't actually call the errback as a task, but will
+instead call the errback function directly so that the raw request, exception
+and traceback objects can be passed to it.
This is an example error callback:
.. code-block:: python
@app.task
- def error_handler(uuid):
- result = AsyncResult(uuid)
- exc = result.get(propagate=False)
+ def error_handler(request, exc, traceback):
print('Task {0} raised exception: {1!r}\n{2!r}'.format(
- uuid, exc, result.traceback))
+ request.id, exc, traceback))
it can be added to the task using the ``link_error`` execution
option:
diff --git a/docs/userguide/canvas.rst b/docs/userguide/canvas.rst
index 10240768435..67c42ba583c 100644
--- a/docs/userguide/canvas.rst
+++ b/docs/userguide/canvas.rst
@@ -569,7 +569,7 @@ Here's an example errback:
def log_error(request, exc, traceback):
with open(os.path.join('/var/errors', request.id), 'a') as fh:
print('--\n\n{0} {1} {2}'.format(
- task_id, exc, traceback), file=fh)
+ request.id, exc, traceback), file=fh)
To make it even easier to link tasks together there's
a special signature called :class:`~celery.chain` that lets
From ee13eae8e20896beadd89dec8f521bd781522416 Mon Sep 17 00:00:00 2001
From: Stuart Axon
Date: Mon, 30 Nov 2020 13:10:46 +0000
Subject: [PATCH 085/415] Update testing.rst (#6507)
Use double back ticks for some code examples, so that quotes don't get converted into smart-quotes.
https://github.com/celery/celery/issues/6497
---
docs/userguide/testing.rst | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/userguide/testing.rst b/docs/userguide/testing.rst
index 330a24d1dc2..1df28b21978 100644
--- a/docs/userguide/testing.rst
+++ b/docs/userguide/testing.rst
@@ -103,10 +103,10 @@ Enabling
Celery initially ships the plugin in a disabled state, to enable it you can either:
- * `pip install celery[pytest]`
- * `pip install pytest-celery`
- * or add an environment variable `PYTEST_PLUGINS=celery.contrib.pytest`
- * or add `pytest_plugins = ("celery.contrib.pytest", )` to your root conftest.py
+ * ``pip install celery[pytest]``
+ * ``pip install pytest-celery``
+ * or add an environment variable ``PYTEST_PLUGINS=celery.contrib.pytest``
+ * or add ``pytest_plugins = ("celery.contrib.pytest", )`` to your root conftest.py
Marks
From 208e90e40f4aa3bfd5bc75600af9d1ed4e1efa28 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 2 Dec 2020 12:46:44 +0200
Subject: [PATCH 086/415] Don't upgrade click to 8.x since click-repl doesn't
support it yet.
Fixes #6511.
Upstream issue: https://github.com/click-contrib/click-repl/issues/72
---
requirements/default.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/default.txt b/requirements/default.txt
index 3eafbb470f5..33c3b6be9f8 100644
--- a/requirements/default.txt
+++ b/requirements/default.txt
@@ -2,7 +2,7 @@ pytz>dev
billiard>=3.6.3.0,<4.0
kombu>=5.0.0,<6.0
vine>=5.0.0,<6.0
-click>=7.0
+click>=7.0,<8.0
click-didyoumean>=0.0.3
click-repl>=0.1.6
click-plugins>=1.1.1
From 3a81c267f9ebc54b39be932607041bc77ece5857 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 2 Dec 2020 15:08:10 +0200
Subject: [PATCH 087/415] Update documentation on changes to custom CLI options
in 5.0.
Fixes #6380.
---
docs/conf.py | 1 +
docs/userguide/extending.rst | 17 +++++++----------
docs/whatsnew-5.0.rst | 2 ++
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index 6c7dbc6aaad..6cc0f92fe64 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,6 +23,7 @@
],
extra_intersphinx_mapping={
'cyanide': ('https://cyanide.readthedocs.io/en/latest', None),
+ 'click': ('https://click.palletsprojects.com/en/7.x/', None),
},
apicheck_ignore_modules=[
'celery.__main__',
diff --git a/docs/userguide/extending.rst b/docs/userguide/extending.rst
index 21ff68ecd2a..443255e1789 100644
--- a/docs/userguide/extending.rst
+++ b/docs/userguide/extending.rst
@@ -729,25 +729,22 @@ You can add additional command-line options to the ``worker``, ``beat``, and
``events`` commands by modifying the :attr:`~@user_options` attribute of the
application instance.
-Celery commands uses the :mod:`argparse` module to parse command-line
-arguments, and so to add custom arguments you need to specify a callback
-that takes a :class:`argparse.ArgumentParser` instance - and adds arguments.
-Please see the :mod:`argparse` documentation to read about the fields supported.
+Celery commands uses the :mod:`click` module to parse command-line
+arguments, and so to add custom arguments you need to add :class:`click.Option` instances
+to the relevant set.
Example adding a custom option to the :program:`celery worker` command:
.. code-block:: python
from celery import Celery
+ from click import Option
app = Celery(broker='amqp://')
- def add_worker_arguments(parser):
- parser.add_argument(
- '--enable-my-option', action='store_true', default=False,
- help='Enable custom option.',
- ),
- app.user_options['worker'].add(add_worker_arguments)
+ app.user_options['worker'].add(Option(('--enable-my-option',),
+ is_flag=True,
+ help='Enable custom option.'))
All bootsteps will now receive this argument as a keyword argument to
diff --git a/docs/whatsnew-5.0.rst b/docs/whatsnew-5.0.rst
index 3f93ce3e979..7e38c924a13 100644
--- a/docs/whatsnew-5.0.rst
+++ b/docs/whatsnew-5.0.rst
@@ -275,6 +275,8 @@ As a result a few breaking changes has been introduced:
- :program:`celery amqp` and :program:`celery shell` require the `repl`
sub command to start a shell. You can now also invoke specific commands
without a shell. Type `celery amqp --help` or `celery shell --help` for details.
+- The API for adding user options has changed.
+ Refer to the :ref:`documentation ` for details.
Click provides shell completion `out of the box `_.
This functionality replaces our previous bash completion script and adds
From 1c076a646ec04b9c920ff75b79a3911096da2838 Mon Sep 17 00:00:00 2001
From: Sonya Chhabra
Date: Wed, 2 Dec 2020 20:16:52 -0500
Subject: [PATCH 088/415] update step to install homebrew
---
docs/getting-started/brokers/rabbitmq.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/getting-started/brokers/rabbitmq.rst b/docs/getting-started/brokers/rabbitmq.rst
index 6f5d95dd8ab..430844bdfec 100644
--- a/docs/getting-started/brokers/rabbitmq.rst
+++ b/docs/getting-started/brokers/rabbitmq.rst
@@ -86,7 +86,7 @@ documentation`_:
.. code-block:: console
- ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Finally, we can install RabbitMQ using :command:`brew`:
From 18a0963ed36f87b8fb884ad27cfc2b7f1ca9f53c Mon Sep 17 00:00:00 2001
From: AbdealiJK
Date: Tue, 10 Nov 2020 10:21:49 +0530
Subject: [PATCH 089/415] redis: Support Sentinel with SSL
Use the SentinelManagedSSLConnection when SSL is enabled for the
transport. The redis-py project doesn't have a connection class for
SSL+Sentinel yet. So, create a class in redis.py to add that
functionality.
---
celery/backends/redis.py | 20 ++++++++++++++++++--
t/unit/backends/test_redis.py | 31 +++++++++++++++++++++++++++++++
2 files changed, 49 insertions(+), 2 deletions(-)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index dd3677f569c..6820047c752 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -185,6 +185,7 @@ class RedisBackend(BaseKeyValueStoreBackend, AsyncBackendMixin):
#: :pypi:`redis` client module.
redis = redis
+ connection_class_ssl = redis.SSLConnection if redis else None
#: Maximum number of connections in the pool.
max_connections = None
@@ -236,7 +237,7 @@ def __init__(self, host=None, port=None, db=None, password=None,
ssl = _get('redis_backend_use_ssl')
if ssl:
self.connparams.update(ssl)
- self.connparams['connection_class'] = redis.SSLConnection
+ self.connparams['connection_class'] = self.connection_class_ssl
if url:
self.connparams = self._params_from_url(url, self.connparams)
@@ -245,7 +246,7 @@ def __init__(self, host=None, port=None, db=None, password=None,
# redis_backend_use_ssl dict, check ssl_cert_reqs is valid. If set
# via query string ssl_cert_reqs will be a string so convert it here
if ('connection_class' in self.connparams and
- self.connparams['connection_class'] is redis.SSLConnection):
+ issubclass(self.connparams['connection_class'], redis.SSLConnection)):
ssl_cert_reqs_missing = 'MISSING'
ssl_string_to_constant = {'CERT_REQUIRED': CERT_REQUIRED,
'CERT_OPTIONAL': CERT_OPTIONAL,
@@ -535,10 +536,25 @@ def __reduce__(self, args=(), kwargs=None):
)
+if getattr(redis, "sentinel", None):
+ class SentinelManagedSSLConnection(
+ redis.sentinel.SentinelManagedConnection,
+ redis.SSLConnection):
+ """Connect to a Redis server using Sentinel + TLS.
+
+ Use Sentinel to identify which Redis server is the current master
+ to connect to and when connecting to the Master server, use an
+ SSL Connection.
+ """
+
+ pass
+
+
class SentinelBackend(RedisBackend):
"""Redis sentinel task result store."""
sentinel = getattr(redis, "sentinel", None)
+ connection_class_ssl = SentinelManagedSSLConnection if sentinel else None
def __init__(self, *args, **kwargs):
if self.sentinel is None:
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index f534077a4fd..3bacc5fcc67 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -1126,3 +1126,34 @@ def test_get_pool(self):
)
pool = x._get_pool(**x.connparams)
assert pool
+
+ def test_backend_ssl(self):
+ pytest.importorskip('redis')
+
+ from celery.backends.redis import SentinelBackend
+ self.app.conf.redis_backend_use_ssl = {
+ 'ssl_cert_reqs': "CERT_REQUIRED",
+ 'ssl_ca_certs': '/path/to/ca.crt',
+ 'ssl_certfile': '/path/to/client.crt',
+ 'ssl_keyfile': '/path/to/client.key',
+ }
+ self.app.conf.redis_socket_timeout = 30.0
+ self.app.conf.redis_socket_connect_timeout = 100.0
+ x = SentinelBackend(
+ 'sentinel://:bosco@vandelay.com:123//1', app=self.app,
+ )
+ assert x.connparams
+ assert len(x.connparams['hosts']) == 1
+ assert x.connparams['hosts'][0]['host'] == 'vandelay.com'
+ assert x.connparams['hosts'][0]['db'] == 1
+ assert x.connparams['hosts'][0]['port'] == 123
+ assert x.connparams['hosts'][0]['password'] == 'bosco'
+ assert x.connparams['socket_timeout'] == 30.0
+ assert x.connparams['socket_connect_timeout'] == 100.0
+ assert x.connparams['ssl_cert_reqs'] == ssl.CERT_REQUIRED
+ assert x.connparams['ssl_ca_certs'] == '/path/to/ca.crt'
+ assert x.connparams['ssl_certfile'] == '/path/to/client.crt'
+ assert x.connparams['ssl_keyfile'] == '/path/to/client.key'
+
+ from celery.backends.redis import SentinelManagedSSLConnection
+ assert x.connparams['connection_class'] is SentinelManagedSSLConnection
From 0fa4db8889325fd774f7e89ebb219a87fc1d8cfb Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 3 Dec 2020 17:54:32 +0200
Subject: [PATCH 090/415] Revert "redis: Support Sentinel with SSL" (#6518)
This reverts commit 18a0963ed36f87b8fb884ad27cfc2b7f1ca9f53c.
---
celery/backends/redis.py | 20 ++------------------
t/unit/backends/test_redis.py | 31 -------------------------------
2 files changed, 2 insertions(+), 49 deletions(-)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index 6820047c752..dd3677f569c 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -185,7 +185,6 @@ class RedisBackend(BaseKeyValueStoreBackend, AsyncBackendMixin):
#: :pypi:`redis` client module.
redis = redis
- connection_class_ssl = redis.SSLConnection if redis else None
#: Maximum number of connections in the pool.
max_connections = None
@@ -237,7 +236,7 @@ def __init__(self, host=None, port=None, db=None, password=None,
ssl = _get('redis_backend_use_ssl')
if ssl:
self.connparams.update(ssl)
- self.connparams['connection_class'] = self.connection_class_ssl
+ self.connparams['connection_class'] = redis.SSLConnection
if url:
self.connparams = self._params_from_url(url, self.connparams)
@@ -246,7 +245,7 @@ def __init__(self, host=None, port=None, db=None, password=None,
# redis_backend_use_ssl dict, check ssl_cert_reqs is valid. If set
# via query string ssl_cert_reqs will be a string so convert it here
if ('connection_class' in self.connparams and
- issubclass(self.connparams['connection_class'], redis.SSLConnection)):
+ self.connparams['connection_class'] is redis.SSLConnection):
ssl_cert_reqs_missing = 'MISSING'
ssl_string_to_constant = {'CERT_REQUIRED': CERT_REQUIRED,
'CERT_OPTIONAL': CERT_OPTIONAL,
@@ -536,25 +535,10 @@ def __reduce__(self, args=(), kwargs=None):
)
-if getattr(redis, "sentinel", None):
- class SentinelManagedSSLConnection(
- redis.sentinel.SentinelManagedConnection,
- redis.SSLConnection):
- """Connect to a Redis server using Sentinel + TLS.
-
- Use Sentinel to identify which Redis server is the current master
- to connect to and when connecting to the Master server, use an
- SSL Connection.
- """
-
- pass
-
-
class SentinelBackend(RedisBackend):
"""Redis sentinel task result store."""
sentinel = getattr(redis, "sentinel", None)
- connection_class_ssl = SentinelManagedSSLConnection if sentinel else None
def __init__(self, *args, **kwargs):
if self.sentinel is None:
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index 3bacc5fcc67..f534077a4fd 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -1126,34 +1126,3 @@ def test_get_pool(self):
)
pool = x._get_pool(**x.connparams)
assert pool
-
- def test_backend_ssl(self):
- pytest.importorskip('redis')
-
- from celery.backends.redis import SentinelBackend
- self.app.conf.redis_backend_use_ssl = {
- 'ssl_cert_reqs': "CERT_REQUIRED",
- 'ssl_ca_certs': '/path/to/ca.crt',
- 'ssl_certfile': '/path/to/client.crt',
- 'ssl_keyfile': '/path/to/client.key',
- }
- self.app.conf.redis_socket_timeout = 30.0
- self.app.conf.redis_socket_connect_timeout = 100.0
- x = SentinelBackend(
- 'sentinel://:bosco@vandelay.com:123//1', app=self.app,
- )
- assert x.connparams
- assert len(x.connparams['hosts']) == 1
- assert x.connparams['hosts'][0]['host'] == 'vandelay.com'
- assert x.connparams['hosts'][0]['db'] == 1
- assert x.connparams['hosts'][0]['port'] == 123
- assert x.connparams['hosts'][0]['password'] == 'bosco'
- assert x.connparams['socket_timeout'] == 30.0
- assert x.connparams['socket_connect_timeout'] == 100.0
- assert x.connparams['ssl_cert_reqs'] == ssl.CERT_REQUIRED
- assert x.connparams['ssl_ca_certs'] == '/path/to/ca.crt'
- assert x.connparams['ssl_certfile'] == '/path/to/client.crt'
- assert x.connparams['ssl_keyfile'] == '/path/to/client.key'
-
- from celery.backends.redis import SentinelManagedSSLConnection
- assert x.connparams['connection_class'] is SentinelManagedSSLConnection
From 4fad9072ff4eca154ab0ac0b76f3a54fd7e738fe Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 3 Dec 2020 18:16:10 +0200
Subject: [PATCH 091/415] Reintroduce support for custom preload options
(#6516)
* Restore preload options.
Fixes #6307.
* Document breaking changes for preload options in 5.0.
Fixes #6379.
---
celery/bin/amqp.py | 3 +++
celery/bin/base.py | 21 +++++++++++++++++++++
celery/bin/beat.py | 4 +++-
celery/bin/call.py | 5 +++--
celery/bin/celery.py | 3 +++
celery/bin/control.py | 6 +++++-
celery/bin/events.py | 5 ++++-
celery/bin/graph.py | 3 ++-
celery/bin/list.py | 3 ++-
celery/bin/logtool.py | 3 ++-
celery/bin/migrate.py | 4 +++-
celery/bin/multi.py | 3 ++-
celery/bin/purge.py | 4 +++-
celery/bin/result.py | 4 +++-
celery/bin/shell.py | 4 +++-
celery/bin/upgrade.py | 4 +++-
celery/bin/worker.py | 4 +++-
docs/userguide/extending.rst | 19 ++++++-------------
18 files changed, 74 insertions(+), 28 deletions(-)
diff --git a/celery/bin/amqp.py b/celery/bin/amqp.py
index e8b7f24066c..ab8ab5f0100 100644
--- a/celery/bin/amqp.py
+++ b/celery/bin/amqp.py
@@ -8,6 +8,8 @@
__all__ = ('amqp',)
+from celery.bin.base import handle_preload_options
+
def dump_message(message):
if message is None:
@@ -54,6 +56,7 @@ def reconnect(self):
@click.group(invoke_without_command=True)
@click.pass_context
+@handle_preload_options
def amqp(ctx):
"""AMQP Administration Shell.
diff --git a/celery/bin/base.py b/celery/bin/base.py
index 52f94382c65..78d6371b420 100644
--- a/celery/bin/base.py
+++ b/celery/bin/base.py
@@ -1,6 +1,7 @@
"""Click customizations for Celery."""
import json
from collections import OrderedDict
+from functools import update_wrapper
from pprint import pformat
import click
@@ -8,6 +9,7 @@
from kombu.utils.objects import cached_property
from celery._state import get_current_app
+from celery.signals import user_preload_options
from celery.utils import text
from celery.utils.log import mlevel
from celery.utils.time import maybe_iso8601
@@ -113,6 +115,25 @@ def say_chat(self, direction, title, body='', show_body=False):
self.echo(body)
+def handle_preload_options(f):
+ def caller(ctx, *args, **kwargs):
+ app = ctx.obj.app
+
+ preload_options = [o.name for o in app.user_options.get('preload', [])]
+
+ if preload_options:
+ user_options = {
+ preload_option: kwargs[preload_option]
+ for preload_option in preload_options
+ }
+
+ user_preload_options.send(sender=f, app=app, options=user_options)
+
+ return f(ctx, *args, **kwargs)
+
+ return update_wrapper(caller, f)
+
+
class CeleryOption(click.Option):
"""Customized option for Celery."""
diff --git a/celery/bin/beat.py b/celery/bin/beat.py
index 54a74c14c7e..145b44e9720 100644
--- a/celery/bin/beat.py
+++ b/celery/bin/beat.py
@@ -3,7 +3,8 @@
import click
-from celery.bin.base import LOG_LEVEL, CeleryDaemonCommand, CeleryOption
+from celery.bin.base import (LOG_LEVEL, CeleryDaemonCommand, CeleryOption,
+ handle_preload_options)
from celery.platforms import detached, maybe_drop_privileges
@@ -43,6 +44,7 @@
help_group="Beat Options",
help="Logging level.")
@click.pass_context
+@handle_preload_options
def beat(ctx, detach=False, logfile=None, pidfile=None, uid=None,
gid=None, umask=None, workdir=None, **kwargs):
"""Start the beat periodic task scheduler."""
diff --git a/celery/bin/call.py b/celery/bin/call.py
index c2744a4cd28..35ca34e3f33 100644
--- a/celery/bin/call.py
+++ b/celery/bin/call.py
@@ -2,9 +2,10 @@
import click
from celery.bin.base import (ISO8601, ISO8601_OR_FLOAT, JSON, CeleryCommand,
- CeleryOption)
+ CeleryOption, handle_preload_options)
+@click.command(cls=CeleryCommand)
@click.argument('name')
@click.option('-a',
'--args',
@@ -52,8 +53,8 @@
cls=CeleryOption,
help_group="Routing Options",
help="custom routing key.")
-@click.command(cls=CeleryCommand)
@click.pass_context
+@handle_preload_options
def call(ctx, name, args, kwargs, eta, countdown, expires, serializer, queue, exchange, routing_key):
"""Call a task by name."""
task_id = ctx.obj.app.send_task(
diff --git a/celery/bin/celery.py b/celery/bin/celery.py
index 095766c0f4d..c6b862d0f10 100644
--- a/celery/bin/celery.py
+++ b/celery/bin/celery.py
@@ -145,6 +145,9 @@ def celery(ctx, app, broker, result_backend, loader, config, workdir,
beat.params.extend(ctx.obj.app.user_options.get('beat', []))
events.params.extend(ctx.obj.app.user_options.get('events', []))
+ for command in celery.commands.values():
+ command.params.extend(ctx.obj.app.user_options.get('preload', []))
+
@celery.command(cls=CeleryCommand)
@click.pass_context
diff --git a/celery/bin/control.py b/celery/bin/control.py
index a48de89ce72..3fe8eb76b42 100644
--- a/celery/bin/control.py
+++ b/celery/bin/control.py
@@ -4,7 +4,8 @@
import click
from kombu.utils.json import dumps
-from celery.bin.base import COMMA_SEPARATED_LIST, CeleryCommand, CeleryOption
+from celery.bin.base import (COMMA_SEPARATED_LIST, CeleryCommand,
+ CeleryOption, handle_preload_options)
from celery.platforms import EX_UNAVAILABLE
from celery.utils import text
from celery.worker.control import Panel
@@ -71,6 +72,7 @@ def _compile_arguments(action, args):
help_group='Remote Control Options',
help='Use json as output format.')
@click.pass_context
+@handle_preload_options
def status(ctx, timeout, destination, json, **kwargs):
"""Show list of workers that are online."""
callback = None if json else partial(_say_remote_command_reply, ctx)
@@ -115,6 +117,7 @@ def status(ctx, timeout, destination, json, **kwargs):
help_group='Remote Control Options',
help='Use json as output format.')
@click.pass_context
+@handle_preload_options
def inspect(ctx, action, timeout, destination, json, **kwargs):
"""Inspect the worker at runtime.
@@ -164,6 +167,7 @@ def inspect(ctx, action, timeout, destination, json, **kwargs):
help_group='Remote Control Options',
help='Use json as output format.')
@click.pass_context
+@handle_preload_options
def control(ctx, action, timeout, destination, json):
"""Workers remote control.
diff --git a/celery/bin/events.py b/celery/bin/events.py
index 0e3bd1a8aea..dc535f5b7b7 100644
--- a/celery/bin/events.py
+++ b/celery/bin/events.py
@@ -4,7 +4,8 @@
import click
-from celery.bin.base import LOG_LEVEL, CeleryDaemonCommand, CeleryOption
+from celery.bin.base import (LOG_LEVEL, CeleryDaemonCommand, CeleryOption,
+ handle_preload_options)
from celery.platforms import detached, set_process_title, strargv
@@ -47,6 +48,7 @@ def _run_evtop(app):
raise click.UsageError("The curses module is required for this command.")
+@handle_preload_options
@click.command(cls=CeleryDaemonCommand)
@click.option('-d',
'--dump',
@@ -78,6 +80,7 @@ def _run_evtop(app):
help_group="Snapshot",
help="Logging level.")
@click.pass_context
+@handle_preload_options
def events(ctx, dump, camera, detach, frequency, maxrate, loglevel, **kwargs):
"""Event-stream utilities."""
app = ctx.obj.app
diff --git a/celery/bin/graph.py b/celery/bin/graph.py
index 3013077b4b5..93b01e808fa 100644
--- a/celery/bin/graph.py
+++ b/celery/bin/graph.py
@@ -4,11 +4,12 @@
import click
-from celery.bin.base import CeleryCommand
+from celery.bin.base import CeleryCommand, handle_preload_options
from celery.utils.graph import DependencyGraph, GraphFormatter
@click.group()
+@handle_preload_options
def graph():
"""The ``celery graph`` command."""
diff --git a/celery/bin/list.py b/celery/bin/list.py
index fefc5e73fde..06c4fbf28bf 100644
--- a/celery/bin/list.py
+++ b/celery/bin/list.py
@@ -1,10 +1,11 @@
"""The ``celery list bindings`` command, used to inspect queue bindings."""
import click
-from celery.bin.base import CeleryCommand
+from celery.bin.base import CeleryCommand, handle_preload_options
@click.group(name="list")
+@handle_preload_options
def list_():
"""Get info from broker.
diff --git a/celery/bin/logtool.py b/celery/bin/logtool.py
index 07dbffa8767..83e8064bdb0 100644
--- a/celery/bin/logtool.py
+++ b/celery/bin/logtool.py
@@ -5,7 +5,7 @@
import click
-from celery.bin.base import CeleryCommand
+from celery.bin.base import CeleryCommand, handle_preload_options
__all__ = ('logtool',)
@@ -111,6 +111,7 @@ def report(self):
@click.group()
+@handle_preload_options
def logtool():
"""The ``celery logtool`` command."""
diff --git a/celery/bin/migrate.py b/celery/bin/migrate.py
index c5ba9b33c43..febaaaacab2 100644
--- a/celery/bin/migrate.py
+++ b/celery/bin/migrate.py
@@ -2,7 +2,8 @@
import click
from kombu import Connection
-from celery.bin.base import CeleryCommand, CeleryOption
+from celery.bin.base import (CeleryCommand, CeleryOption,
+ handle_preload_options)
from celery.contrib.migrate import migrate_tasks
@@ -44,6 +45,7 @@
help_group='Migration Options',
help='Continually migrate tasks until killed.')
@click.pass_context
+@handle_preload_options
def migrate(ctx, source, destination, **kwargs):
"""Migrate tasks from one broker to another.
diff --git a/celery/bin/multi.py b/celery/bin/multi.py
index 12bb52b87d2..82a86a6129e 100644
--- a/celery/bin/multi.py
+++ b/celery/bin/multi.py
@@ -108,7 +108,7 @@
from celery import VERSION_BANNER
from celery.apps.multi import Cluster, MultiParser, NamespacedOptionParser
-from celery.bin.base import CeleryCommand
+from celery.bin.base import CeleryCommand, handle_preload_options
from celery.platforms import EX_FAILURE, EX_OK, signals
from celery.utils import term
from celery.utils.text import pluralize
@@ -468,6 +468,7 @@ def DOWN(self):
}
)
@click.pass_context
+@handle_preload_options
def multi(ctx):
"""Start multiple worker instances."""
cmd = MultiTool(quiet=ctx.obj.quiet, no_color=ctx.obj.no_color)
diff --git a/celery/bin/purge.py b/celery/bin/purge.py
index 609a9a0f660..2629ac7eff3 100644
--- a/celery/bin/purge.py
+++ b/celery/bin/purge.py
@@ -1,7 +1,8 @@
"""The ``celery purge`` program, used to delete messages from queues."""
import click
-from celery.bin.base import COMMA_SEPARATED_LIST, CeleryCommand, CeleryOption
+from celery.bin.base import (COMMA_SEPARATED_LIST, CeleryCommand,
+ CeleryOption, handle_preload_options)
from celery.utils import text
@@ -25,6 +26,7 @@
help_group='Purging Options',
help="Comma separated list of queues names not to purge.")
@click.pass_context
+@handle_preload_options
def purge(ctx, force, queues, exclude_queues):
"""Erase all messages from all known task queues.
diff --git a/celery/bin/result.py b/celery/bin/result.py
index d90421c4cde..c126fb588ee 100644
--- a/celery/bin/result.py
+++ b/celery/bin/result.py
@@ -1,7 +1,8 @@
"""The ``celery result`` program, used to inspect task results."""
import click
-from celery.bin.base import CeleryCommand, CeleryOption
+from celery.bin.base import (CeleryCommand, CeleryOption,
+ handle_preload_options)
@click.command(cls=CeleryCommand)
@@ -17,6 +18,7 @@
help_group='Result Options',
help="Show traceback instead.")
@click.pass_context
+@handle_preload_options
def result(ctx, task_id, task, traceback):
"""Print the return value for a given task id."""
app = ctx.obj.app
diff --git a/celery/bin/shell.py b/celery/bin/shell.py
index b3b77e02fdb..378448a24cf 100644
--- a/celery/bin/shell.py
+++ b/celery/bin/shell.py
@@ -6,7 +6,8 @@
import click
-from celery.bin.base import CeleryCommand, CeleryOption
+from celery.bin.base import (CeleryCommand, CeleryOption,
+ handle_preload_options)
def _invoke_fallback_shell(locals):
@@ -114,6 +115,7 @@ def _invoke_default_shell(locals):
help_group="Shell Options",
help="Use gevent.")
@click.pass_context
+@handle_preload_options
def shell(ctx, ipython=False, bpython=False,
python=False, without_tasks=False, eventlet=False,
gevent=False):
diff --git a/celery/bin/upgrade.py b/celery/bin/upgrade.py
index 1518297172c..e083995b674 100644
--- a/celery/bin/upgrade.py
+++ b/celery/bin/upgrade.py
@@ -5,11 +5,13 @@
import click
from celery.app import defaults
-from celery.bin.base import CeleryCommand, CeleryOption
+from celery.bin.base import (CeleryCommand, CeleryOption,
+ handle_preload_options)
from celery.utils.functional import pass1
@click.group()
+@handle_preload_options
def upgrade():
"""Perform upgrade between versions."""
diff --git a/celery/bin/worker.py b/celery/bin/worker.py
index cd826b89b17..ca16a19b4e3 100644
--- a/celery/bin/worker.py
+++ b/celery/bin/worker.py
@@ -9,7 +9,8 @@
from celery import concurrency
from celery.bin.base import (COMMA_SEPARATED_LIST, LOG_LEVEL,
- CeleryDaemonCommand, CeleryOption)
+ CeleryDaemonCommand, CeleryOption,
+ handle_preload_options)
from celery.platforms import (EX_FAILURE, EX_OK, detached,
maybe_drop_privileges)
from celery.utils.log import get_logger
@@ -273,6 +274,7 @@ def detach(path, argv, logfile=None, pidfile=None, uid=None,
cls=CeleryOption,
help_group="Embedded Beat Options")
@click.pass_context
+@handle_preload_options
def worker(ctx, hostname=None, pool_cls=None, app=None, uid=None, gid=None,
loglevel=None, logfile=None, pidfile=None, statedb=None,
**kwargs):
diff --git a/docs/userguide/extending.rst b/docs/userguide/extending.rst
index 443255e1789..cf3a9929be8 100644
--- a/docs/userguide/extending.rst
+++ b/docs/userguide/extending.rst
@@ -769,29 +769,22 @@ Preload options
~~~~~~~~~~~~~~~
The :program:`celery` umbrella command supports the concept of 'preload
-options'. These are special options passed to all sub-commands and parsed
-outside of the main parsing step.
+options'. These are special options passed to all sub-commands.
-The list of default preload options can be found in the API reference:
-:mod:`celery.bin.base`.
-
-You can add new preload options too, for example to specify a configuration
+You can add new preload options, for example to specify a configuration
template:
.. code-block:: python
from celery import Celery
from celery import signals
- from celery.bin import Option
+ from click import Option
app = Celery()
- def add_preload_options(parser):
- parser.add_argument(
- '-Z', '--template', default='default',
- help='Configuration template to use.',
- )
- app.user_options['preload'].add(add_preload_options)
+ app.user_options['preload'].add(Option(('-Z', '--template'),
+ default='default',
+ help='Configuration template to use.'))
@signals.user_preload_options.connect
def on_preload_parsed(options, **kwargs):
From 39db90cc83b8a283933fb5a6d1b16b46837d1ced Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 3 Dec 2020 18:19:47 +0200
Subject: [PATCH 092/415] Changelog for 5.0.3.
---
Changelog.rst | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/Changelog.rst b/Changelog.rst
index b65686a6708..407d9e4acc3 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -8,6 +8,22 @@ This document contains change notes for bugfix & new features
in the 5.0.x series, please see :ref:`whatsnew-5.0` for
an overview of what's new in Celery 5.0.
+.. _version-5.0.3:
+
+5.0.3
+=====
+:release-date: 2020-12-03 6.30 P.M UTC+2:00
+:release-by: Omer Katz
+
+- Make `--workdir` eager for early handling (#6457).
+- When using the MongoDB backend, don't cleanup if result_expires is 0 or None (#6462).
+- Fix passing queues into purge command (#6469).
+- Restore `app.start()` and `app.worker_main()` (#6481).
+- Detaching no longer creates an extra log file (#6426).
+- Result backend instances are now thread local to ensure thread safety (#6416).
+- Don't upgrade click to 8.x since click-repl doesn't support it yet.
+- Restore preload options (#6516).
+
.. _version-5.0.2:
5.0.2
From a4d942b3156961a8fdd6829121bdc52fc99da30a Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 3 Dec 2020 18:20:36 +0200
Subject: [PATCH 093/415] =?UTF-8?q?Bump=20version:=205.0.2=20=E2=86=92=205?=
=?UTF-8?q?.0.3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.bumpversion.cfg | 2 +-
README.rst | 6 +++---
celery/__init__.py | 2 +-
docs/includes/introduction.txt | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 7be80a9bab6..6ea6b829c07 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 5.0.2
+current_version = 5.0.3
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z\d]+)?
diff --git a/README.rst b/README.rst
index 529669641d9..31c09d27b39 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
|build-status| |coverage| |license| |wheel| |pyversion| |pyimp| |ocbackerbadge| |ocsponsorbadge|
-:Version: 5.0.2 (singularity)
+:Version: 5.0.3 (singularity)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
@@ -57,7 +57,7 @@ in such a way that the client enqueues an URL to be requested by a worker.
What do I need?
===============
-Celery version 5.0.2 runs on,
+Celery version 5.0.3 runs on,
- Python (3.6, 3.7, 3.8)
- PyPy3.6 (7.6)
@@ -89,7 +89,7 @@ Get Started
===========
If this is the first time you're trying to use Celery, or you're
-new to Celery 5.0.2 coming from previous versions then you should read our
+new to Celery 5.0.3 coming from previous versions then you should read our
getting started tutorials:
- `First steps with Celery`_
diff --git a/celery/__init__.py b/celery/__init__.py
index 7ed8e28cb0a..b4d9bb899a8 100644
--- a/celery/__init__.py
+++ b/celery/__init__.py
@@ -17,7 +17,7 @@
SERIES = 'singularity'
-__version__ = '5.0.2'
+__version__ = '5.0.3'
__author__ = 'Ask Solem'
__contact__ = 'auvipy@gmail.com'
__homepage__ = 'http://celeryproject.org'
diff --git a/docs/includes/introduction.txt b/docs/includes/introduction.txt
index a19bd2a012a..70751c92c17 100644
--- a/docs/includes/introduction.txt
+++ b/docs/includes/introduction.txt
@@ -1,4 +1,4 @@
-:Version: 5.0.2 (cliffs)
+:Version: 5.0.3 (cliffs)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
From a192f9cbf546e36b590166426d5e26a90964eeb1 Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Sun, 6 Dec 2020 11:03:31 +0100
Subject: [PATCH 094/415] Added integration tests for calling a task (#6523)
---
t/integration/tasks.py | 41 ++++++--
t/integration/test_tasks.py | 200 +++++++++++++++++++++++++++++++++++-
2 files changed, 232 insertions(+), 9 deletions(-)
diff --git a/t/integration/tasks.py b/t/integration/tasks.py
index 1aaeed32378..2b4937a3725 100644
--- a/t/integration/tasks.py
+++ b/t/integration/tasks.py
@@ -16,15 +16,18 @@ def identity(x):
@shared_task
-def add(x, y):
- """Add two numbers."""
- return x + y
+def add(x, y, z=None):
+ """Add two or three numbers."""
+ if z:
+ return x + y + z
+ else:
+ return x + y
-@shared_task
-def raise_error(*args):
- """Deliberately raise an error."""
- raise ValueError("deliberate error")
+@shared_task(typing=False)
+def add_not_typed(x, y):
+ """Add two numbers, but don't check arguments"""
+ return x + y
@shared_task(ignore_result=True)
@@ -33,6 +36,12 @@ def add_ignore_result(x, y):
return x + y
+@shared_task
+def raise_error(*args):
+ """Deliberately raise an error."""
+ raise ValueError("deliberate error")
+
+
@shared_task
def chain_add(x, y):
(
@@ -162,6 +171,24 @@ def collect_ids(self, res, i):
return res, (self.request.root_id, self.request.parent_id, i)
+@shared_task(bind=True, default_retry_delay=1)
+def retry(self, return_value=None):
+ """Task simulating multiple retries.
+
+ When return_value is provided, the task after retries returns
+ the result. Otherwise it fails.
+ """
+ if return_value:
+ attempt = getattr(self, 'attempt', 0)
+ print('attempt', attempt)
+ if attempt >= 3:
+ delattr(self, 'attempt')
+ return return_value
+ self.attempt = attempt + 1
+
+ raise self.retry(exc=ExpectedException(), countdown=5)
+
+
@shared_task(bind=True, expires=60.0, max_retries=1)
def retry_once(self, *args, expires=60.0, max_retries=1, countdown=0.1):
"""Task that fails and is retried. Returns the number of retries."""
diff --git a/t/integration/test_tasks.py b/t/integration/test_tasks.py
index edfda576f5b..ca71196a283 100644
--- a/t/integration/test_tasks.py
+++ b/t/integration/test_tasks.py
@@ -1,10 +1,14 @@
+from datetime import datetime, timedelta
+from time import sleep, perf_counter
+
import pytest
+import celery
from celery import group
from .conftest import get_active_redis_channels
-from .tasks import (ClassBasedAutoRetryTask, add, add_ignore_result,
- print_unicode, retry_once, retry_once_priority, sleeping)
+from .tasks import (ClassBasedAutoRetryTask, add, add_ignore_result, add_not_typed, retry,
+ print_unicode, retry_once, retry_once_priority, sleeping, fail, ExpectedException)
TIMEOUT = 10
@@ -28,8 +32,200 @@ def test_class_based_task_retried(self, celery_session_app,
assert res.get(timeout=TIMEOUT) == 1
+def _producer(j):
+ """Single producer helper function"""
+ results = []
+ for i in range(20):
+ results.append([i + j, add.delay(i, j)])
+ for expected, result in results:
+ value = result.get(timeout=10)
+ assert value == expected
+ assert result.status == 'SUCCESS'
+ assert result.ready() is True
+ assert result.successful() is True
+ return j
+
+
class test_tasks:
+ def test_simple_call(self):
+ """Tests direct simple call of task"""
+ assert add(1, 1) == 2
+ assert add(1, 1, z=1) == 3
+
+ @flaky
+ def test_basic_task(self, manager):
+ """Tests basic task call"""
+ results = []
+ # Tests calling task only with args
+ for i in range(10):
+ results.append([i + i, add.delay(i, i)])
+ for expected, result in results:
+ value = result.get(timeout=10)
+ assert value == expected
+ assert result.status == 'SUCCESS'
+ assert result.ready() is True
+ assert result.successful() is True
+
+ results = []
+ # Tests calling task with args and kwargs
+ for i in range(10):
+ results.append([3*i, add.delay(i, i, z=i)])
+ for expected, result in results:
+ value = result.get(timeout=10)
+ assert value == expected
+ assert result.status == 'SUCCESS'
+ assert result.ready() is True
+ assert result.successful() is True
+
+ @flaky
+ def test_multiprocess_producer(self, manager):
+ """Testing multiple processes calling tasks."""
+ from multiprocessing import Pool
+ pool = Pool(20)
+ ret = pool.map(_producer, range(120))
+ assert list(ret) == list(range(120))
+
+ @flaky
+ def test_multithread_producer(self, manager):
+ """Testing multiple threads calling tasks."""
+ from multiprocessing.pool import ThreadPool
+ pool = ThreadPool(20)
+ ret = pool.map(_producer, range(120))
+ assert list(ret) == list(range(120))
+
+ @flaky
+ def test_ignore_result(self, manager):
+ """Testing calling task with ignoring results."""
+ result = add.apply_async((1, 2), ignore_result=True)
+ assert result.get() is None
+
+ @flaky
+ def test_timeout(self, manager):
+ """Testing timeout of getting results from tasks."""
+ result = sleeping.delay(10)
+ with pytest.raises(celery.exceptions.TimeoutError):
+ result.get(timeout=5)
+
+ @flaky
+ def test_expired(self, manager):
+ """Testing expiration of task."""
+ # Fill the queue with tasks which took > 1 sec to process
+ for _ in range(4):
+ sleeping.delay(2)
+ # Execute task with expiration = 1 sec
+ result = add.apply_async((1, 1), expires=1)
+ with pytest.raises(celery.exceptions.TaskRevokedError):
+ result.get()
+ assert result.status == 'REVOKED'
+ assert result.ready() is True
+ assert result.failed() is False
+ assert result.successful() is False
+
+ # Fill the queue with tasks which took > 1 sec to process
+ for _ in range(4):
+ sleeping.delay(2)
+ # Execute task with expiration at now + 1 sec
+ result = add.apply_async((1, 1), expires=datetime.utcnow() + timedelta(seconds=1))
+ with pytest.raises(celery.exceptions.TaskRevokedError):
+ result.get()
+ assert result.status == 'REVOKED'
+ assert result.ready() is True
+ assert result.failed() is False
+ assert result.successful() is False
+
+ @flaky
+ def test_eta(self, manager):
+ """Tests tasks scheduled at some point in future."""
+ start = perf_counter()
+ # Schedule task to be executed in 3 seconds
+ result = add.apply_async((1, 1), countdown=3)
+ sleep(1)
+ assert result.status == 'PENDING'
+ assert result.ready() is False
+ assert result.get() == 2
+ end = perf_counter()
+ assert result.status == 'SUCCESS'
+ assert result.ready() is True
+ # Difference between calling the task and result must be bigger than 3 secs
+ assert (end - start) > 3
+
+ start = perf_counter()
+ # Schedule task to be executed at time now + 3 seconds
+ result = add.apply_async((2, 2), eta=datetime.utcnow() + timedelta(seconds=3))
+ sleep(1)
+ assert result.status == 'PENDING'
+ assert result.ready() is False
+ assert result.get() == 4
+ end = perf_counter()
+ assert result.status == 'SUCCESS'
+ assert result.ready() is True
+ # Difference between calling the task and result must be bigger than 3 secs
+ assert (end - start) > 3
+
+ @flaky
+ def test_fail(self, manager):
+ """Tests that the failing task propagates back correct exception."""
+ result = fail.delay()
+ with pytest.raises(ExpectedException):
+ result.get(timeout=5)
+ assert result.status == 'FAILURE'
+ assert result.ready() is True
+ assert result.failed() is True
+ assert result.successful() is False
+
+ @flaky
+ def test_wrong_arguments(self, manager):
+ """Tests that proper exceptions are raised when task is called with wrong arguments."""
+ with pytest.raises(TypeError):
+ add(5)
+
+ with pytest.raises(TypeError):
+ add(5, 5, wrong_arg=5)
+
+ with pytest.raises(TypeError):
+ add.delay(5)
+
+ with pytest.raises(TypeError):
+ add.delay(5, wrong_arg=5)
+
+ # Tasks with typing=False are not checked but execution should fail
+ result = add_not_typed.delay(5)
+ with pytest.raises(TypeError):
+ result.get(timeout=5)
+ assert result.status == 'FAILURE'
+
+ result = add_not_typed.delay(5, wrong_arg=5)
+ with pytest.raises(TypeError):
+ result.get(timeout=5)
+ assert result.status == 'FAILURE'
+
+ @flaky
+ def test_retry(self, manager):
+ """Tests retrying of task."""
+ # Tests when max. retries is reached
+ result = retry.delay()
+ for _ in range(5):
+ status = result.status
+ if status != 'PENDING':
+ break
+ sleep(1)
+ assert status == 'RETRY'
+ with pytest.raises(ExpectedException):
+ result.get()
+ assert result.status == 'FAILURE'
+
+ # Tests when task is retried but after returns correct result
+ result = retry.delay(return_value='bar')
+ for _ in range(5):
+ status = result.status
+ if status != 'PENDING':
+ break
+ sleep(1)
+ assert status == 'RETRY'
+ assert result.get() == 'bar'
+ assert result.status == 'SUCCESS'
+
@flaky
def test_task_accepted(self, manager, sleep=1):
r1 = sleeping.delay(sleep)
From c3e041050ae252be79d9b4ae400ec0c5b2831d14 Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Mon, 7 Dec 2020 12:27:59 +0100
Subject: [PATCH 095/415] DummyClient of cache+memory:// backend now shares
state between threads (#6524)
---
celery/backends/cache.py | 6 +++++-
t/unit/backends/test_cache.py | 10 ++++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/celery/backends/cache.py b/celery/backends/cache.py
index 01ac1ac3e5f..e340f31b7f6 100644
--- a/celery/backends/cache.py
+++ b/celery/backends/cache.py
@@ -20,6 +20,10 @@
Please use one of the following backends instead: {1}\
"""
+# Global shared in-memory cache for in-memory cache client
+# This is to share cache between threads
+_DUMMY_CLIENT_CACHE = LRUCache(limit=5000)
+
def import_best_memcache():
if _imp[0] is None:
@@ -53,7 +57,7 @@ def Client(*args, **kwargs): # noqa
class DummyClient:
def __init__(self, *args, **kwargs):
- self.cache = LRUCache(limit=5000)
+ self.cache = _DUMMY_CLIENT_CACHE
def get(self, key, *args, **kwargs):
return self.cache.get(key)
diff --git a/t/unit/backends/test_cache.py b/t/unit/backends/test_cache.py
index 6bd23d9d3d2..8400729017d 100644
--- a/t/unit/backends/test_cache.py
+++ b/t/unit/backends/test_cache.py
@@ -35,6 +35,16 @@ def test_no_backend(self):
with pytest.raises(ImproperlyConfigured):
CacheBackend(backend=None, app=self.app)
+ def test_memory_client_is_shared(self):
+ """This test verifies that memory:// backend state is shared over multiple threads"""
+ from threading import Thread
+ t = Thread(
+ target=lambda: CacheBackend(backend='memory://', app=self.app).set('test', 12345)
+ )
+ t.start()
+ t.join()
+ assert self.tb.client.get('test') == 12345
+
def test_mark_as_done(self):
assert self.tb.get_state(self.tid) == states.PENDING
assert self.tb.get_result(self.tid) is None
From f9ccba9160705ae18742a0923b6e574a2fcee097 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 8 Dec 2020 14:38:59 +0200
Subject: [PATCH 096/415] isort.
---
t/integration/test_tasks.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/t/integration/test_tasks.py b/t/integration/test_tasks.py
index ca71196a283..ba5f4fbba77 100644
--- a/t/integration/test_tasks.py
+++ b/t/integration/test_tasks.py
@@ -1,5 +1,5 @@
from datetime import datetime, timedelta
-from time import sleep, perf_counter
+from time import perf_counter, sleep
import pytest
@@ -7,8 +7,9 @@
from celery import group
from .conftest import get_active_redis_channels
-from .tasks import (ClassBasedAutoRetryTask, add, add_ignore_result, add_not_typed, retry,
- print_unicode, retry_once, retry_once_priority, sleeping, fail, ExpectedException)
+from .tasks import (ClassBasedAutoRetryTask, ExpectedException, add,
+ add_ignore_result, add_not_typed, fail, print_unicode,
+ retry, retry_once, retry_once_priority, sleeping)
TIMEOUT = 10
From 0674684dfd6cc29d3b5dbb6d2073895e12bfd2c9 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 8 Dec 2020 14:40:15 +0200
Subject: [PATCH 097/415] Update changelog.
---
Changelog.rst | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/Changelog.rst b/Changelog.rst
index 407d9e4acc3..ba46d1d59ba 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -8,6 +8,20 @@ This document contains change notes for bugfix & new features
in the 5.0.x series, please see :ref:`whatsnew-5.0` for
an overview of what's new in Celery 5.0.
+.. _version-5.0.4:
+
+5.0.4
+=====
+:release-date: 2020-12-08 2.40 P.M UTC+2:00
+:release-by: Omer Katz
+
+- DummyClient of cache+memory:// backend now shares state between threads (#6524).
+
+ This fixes a problem when using our pytest integration with the in memory
+ result backend.
+ Because the state wasn't shared between threads, #6416 results in test suites
+ hanging on `result.get()`.
+
.. _version-5.0.3:
5.0.3
From 3bb2d58620c5e83ad7cdc18cdfe917dccde74088 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 8 Dec 2020 14:40:25 +0200
Subject: [PATCH 098/415] =?UTF-8?q?Bump=20version:=205.0.3=20=E2=86=92=205?=
=?UTF-8?q?.0.4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.bumpversion.cfg | 2 +-
README.rst | 6 +++---
celery/__init__.py | 2 +-
docs/includes/introduction.txt | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 6ea6b829c07..14682ce6b9a 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 5.0.3
+current_version = 5.0.4
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z\d]+)?
diff --git a/README.rst b/README.rst
index 31c09d27b39..22a9fc115bd 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
|build-status| |coverage| |license| |wheel| |pyversion| |pyimp| |ocbackerbadge| |ocsponsorbadge|
-:Version: 5.0.3 (singularity)
+:Version: 5.0.4 (singularity)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
@@ -57,7 +57,7 @@ in such a way that the client enqueues an URL to be requested by a worker.
What do I need?
===============
-Celery version 5.0.3 runs on,
+Celery version 5.0.4 runs on,
- Python (3.6, 3.7, 3.8)
- PyPy3.6 (7.6)
@@ -89,7 +89,7 @@ Get Started
===========
If this is the first time you're trying to use Celery, or you're
-new to Celery 5.0.3 coming from previous versions then you should read our
+new to Celery 5.0.4 coming from previous versions then you should read our
getting started tutorials:
- `First steps with Celery`_
diff --git a/celery/__init__.py b/celery/__init__.py
index b4d9bb899a8..c0feb1712db 100644
--- a/celery/__init__.py
+++ b/celery/__init__.py
@@ -17,7 +17,7 @@
SERIES = 'singularity'
-__version__ = '5.0.3'
+__version__ = '5.0.4'
__author__ = 'Ask Solem'
__contact__ = 'auvipy@gmail.com'
__homepage__ = 'http://celeryproject.org'
diff --git a/docs/includes/introduction.txt b/docs/includes/introduction.txt
index 70751c92c17..ec37039072f 100644
--- a/docs/includes/introduction.txt
+++ b/docs/includes/introduction.txt
@@ -1,4 +1,4 @@
-:Version: 5.0.3 (cliffs)
+:Version: 5.0.4 (cliffs)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
From 420e3931a63538bd225ef57916deccf53cbcb57a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Franti=C5=A1ek=20Zatloukal?=
Date: Tue, 8 Dec 2020 17:18:29 +0100
Subject: [PATCH 099/415] Change deprecated from collections import
Mapping/MutableMapping to from collections.abc ... (#6532)
---
t/unit/utils/test_collections.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/t/unit/utils/test_collections.py b/t/unit/utils/test_collections.py
index 1830c7ce7cd..20005288cee 100644
--- a/t/unit/utils/test_collections.py
+++ b/t/unit/utils/test_collections.py
@@ -1,5 +1,5 @@
import pickle
-from collections import Mapping
+from collections.abc import Mapping
from itertools import count
from time import monotonic
@@ -129,11 +129,11 @@ def test_len(self):
assert len(self.view) == 2
def test_isa_mapping(self):
- from collections import Mapping
+ from collections.abc import Mapping
assert issubclass(ConfigurationView, Mapping)
def test_isa_mutable_mapping(self):
- from collections import MutableMapping
+ from collections.abc import MutableMapping
assert issubclass(ConfigurationView, MutableMapping)
From 5fa063afce60f904120cba7f8a4ac5ee0e722b15 Mon Sep 17 00:00:00 2001
From: elonzh
Date: Thu, 10 Dec 2020 00:05:34 +0800
Subject: [PATCH 100/415] fix #6047
---
docs/django/first-steps-with-django.rst | 2 +-
docs/userguide/configuration.rst | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/docs/django/first-steps-with-django.rst b/docs/django/first-steps-with-django.rst
index 55d64c990eb..f3a20b18a48 100644
--- a/docs/django/first-steps-with-django.rst
+++ b/docs/django/first-steps-with-django.rst
@@ -210,7 +210,7 @@ To use this with your project you need to follow these steps:
.. code-block:: python
- CELERY_CACHE_BACKEND = 'django-cache'
+ CELERY_RESULT_BACKEND = 'django-cache'
We can also use the cache defined in the CACHES setting in django.
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index e9c1c76c151..7142cd6ac16 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -1028,9 +1028,13 @@ setting:
``cache_backend``
~~~~~~~~~~~~~~~~~
-This setting is no longer used as it's now possible to specify
+This setting is no longer used in celery's builtin backends as it's now possible to specify
the cache backend directly in the :setting:`result_backend` setting.
+.. note::
+
+ The :ref:`django-celery-results` library uses ``cache_backend`` for choosing django caches.
+
.. _conf-mongodb-result-backend:
MongoDB backend settings
From 8bceb446e6a07682d4b8dd6199cdac450bd63578 Mon Sep 17 00:00:00 2001
From: Sven Koitka
Date: Thu, 10 Dec 2020 12:42:32 +0100
Subject: [PATCH 101/415] Fix type error in S3 backend (#6537)
* Convert key from bytes to str
* Add unit test for S3 delete of key with type bytes
---
celery/backends/s3.py | 1 +
t/unit/backends/test_s3.py | 11 ++++++-----
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/celery/backends/s3.py b/celery/backends/s3.py
index c102073ccca..ea04ae373d1 100644
--- a/celery/backends/s3.py
+++ b/celery/backends/s3.py
@@ -72,6 +72,7 @@ def set(self, key, value):
s3_object.put(Body=value)
def delete(self, key):
+ key = bytes_to_str(key)
s3_object = self._get_s3_object(key)
s3_object.delete()
diff --git a/t/unit/backends/test_s3.py b/t/unit/backends/test_s3.py
index 5733bb6fca4..fdea04b32cc 100644
--- a/t/unit/backends/test_s3.py
+++ b/t/unit/backends/test_s3.py
@@ -140,8 +140,9 @@ def test_with_error_while_getting_key(self, mock_boto3):
with pytest.raises(ClientError):
s3_backend.get('uuidddd')
+ @pytest.mark.parametrize("key", ['uuid', b'uuid'])
@mock_s3
- def test_delete_a_key(self):
+ def test_delete_a_key(self, key):
self._mock_s3_resource()
self.app.conf.s3_access_key_id = 'somekeyid'
@@ -149,12 +150,12 @@ def test_delete_a_key(self):
self.app.conf.s3_bucket = 'bucket'
s3_backend = S3Backend(app=self.app)
- s3_backend._set_with_state('uuid', 'another_status', states.SUCCESS)
- assert s3_backend.get('uuid') == 'another_status'
+ s3_backend._set_with_state(key, 'another_status', states.SUCCESS)
+ assert s3_backend.get(key) == 'another_status'
- s3_backend.delete('uuid')
+ s3_backend.delete(key)
- assert s3_backend.get('uuid') is None
+ assert s3_backend.get(key) is None
@mock_s3
def test_with_a_non_existing_bucket(self):
From 7d59e50d87d260c9459cbc890e3bce0592dd5f99 Mon Sep 17 00:00:00 2001
From: Arnon Yaari
Date: Tue, 15 Dec 2020 16:29:11 +0200
Subject: [PATCH 102/415] events.py: Remove duplicate decorator in wrong place
(#6543)
`@handle_preload_options` was specified twice as a decorator of `events`, once at the top (wrong) and once at the bottom (right).
This fixes the `celery events` commands and also `celery --help`
---
celery/bin/events.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/celery/bin/events.py b/celery/bin/events.py
index dc535f5b7b7..26b67374aad 100644
--- a/celery/bin/events.py
+++ b/celery/bin/events.py
@@ -48,7 +48,6 @@ def _run_evtop(app):
raise click.UsageError("The curses module is required for this command.")
-@handle_preload_options
@click.command(cls=CeleryDaemonCommand)
@click.option('-d',
'--dump',
From 8aa4eb8e7a2c5874f007b40604193b56871f5368 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 16 Dec 2020 17:32:36 +0200
Subject: [PATCH 103/415] Update changelog.
---
Changelog.rst | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/Changelog.rst b/Changelog.rst
index ba46d1d59ba..0bdb9947f8c 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -8,6 +8,16 @@ This document contains change notes for bugfix & new features
in the 5.0.x series, please see :ref:`whatsnew-5.0` for
an overview of what's new in Celery 5.0.
+.. _version-5.0.5:
+
+5.0.5
+=====
+:release-date: 2020-12-16 5.35 P.M UTC+2:00
+:release-by: Omer Katz
+
+- Ensure keys are strings when deleting results from S3 (#6537).
+- Fix a regression breaking `celery --help` and `celery events` (#6543).
+
.. _version-5.0.4:
5.0.4
From 8492b75c579564c2af5c2be75fe4b2118ebd0cd1 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 16 Dec 2020 17:33:33 +0200
Subject: [PATCH 104/415] =?UTF-8?q?Bump=20version:=205.0.4=20=E2=86=92=205?=
=?UTF-8?q?.0.5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.bumpversion.cfg | 2 +-
README.rst | 6 +++---
celery/__init__.py | 2 +-
docs/includes/introduction.txt | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 14682ce6b9a..0ce811df412 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 5.0.4
+current_version = 5.0.5
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z\d]+)?
diff --git a/README.rst b/README.rst
index 22a9fc115bd..e1cdae5ee0e 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
|build-status| |coverage| |license| |wheel| |pyversion| |pyimp| |ocbackerbadge| |ocsponsorbadge|
-:Version: 5.0.4 (singularity)
+:Version: 5.0.5 (singularity)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
@@ -57,7 +57,7 @@ in such a way that the client enqueues an URL to be requested by a worker.
What do I need?
===============
-Celery version 5.0.4 runs on,
+Celery version 5.0.5 runs on,
- Python (3.6, 3.7, 3.8)
- PyPy3.6 (7.6)
@@ -89,7 +89,7 @@ Get Started
===========
If this is the first time you're trying to use Celery, or you're
-new to Celery 5.0.4 coming from previous versions then you should read our
+new to Celery 5.0.5 coming from previous versions then you should read our
getting started tutorials:
- `First steps with Celery`_
diff --git a/celery/__init__.py b/celery/__init__.py
index c0feb1712db..ae3388c0e56 100644
--- a/celery/__init__.py
+++ b/celery/__init__.py
@@ -17,7 +17,7 @@
SERIES = 'singularity'
-__version__ = '5.0.4'
+__version__ = '5.0.5'
__author__ = 'Ask Solem'
__contact__ = 'auvipy@gmail.com'
__homepage__ = 'http://celeryproject.org'
diff --git a/docs/includes/introduction.txt b/docs/includes/introduction.txt
index ec37039072f..11a99ec278b 100644
--- a/docs/includes/introduction.txt
+++ b/docs/includes/introduction.txt
@@ -1,4 +1,4 @@
-:Version: 5.0.4 (cliffs)
+:Version: 5.0.5 (cliffs)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
From 491054f2724141cbff20731753379459af033bfd Mon Sep 17 00:00:00 2001
From: Hilmar Hilmarsson
Date: Fri, 18 Dec 2020 18:11:05 +0000
Subject: [PATCH 105/415] Add sentinel_kwargs to Rendis Sentinel docs
If the Sentinel cluster has a password, it also has to be passed down via the `sentinel_kwargs` option. If it was not supplied I got an error: `No master found for 'mymaster'`.
Google didn't do much for me trying to find this option, I found it in the source, so I think it should be documented.
---
docs/getting-started/brokers/redis.rst | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/docs/getting-started/brokers/redis.rst b/docs/getting-started/brokers/redis.rst
index ba4b31aa9bd..9d42397de57 100644
--- a/docs/getting-started/brokers/redis.rst
+++ b/docs/getting-started/brokers/redis.rst
@@ -58,6 +58,12 @@ It is also easy to connect directly to a list of Redis Sentinel:
app.conf.broker_url = 'sentinel://localhost:26379;sentinel://localhost:26380;sentinel://localhost:26381'
app.conf.broker_transport_options = { 'master_name': "cluster1" }
+Additional options can be passed to the Sentinel client using ``sentinel_kwargs``:
+
+.. code-block:: python
+
+ app.conf.broker_transport_options = { 'sentinel_kwargs': { 'password': "password" } }
+
.. _redis-visibility_timeout:
Visibility Timeout
From ae463025c12d78c2b96a885aa4385ff33811c17a Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 27 Dec 2020 02:08:32 +0200
Subject: [PATCH 106/415] Depend on the maintained python-consul2 library.
(#6544)
python-consul has not been maintained in a long while now.
python-consul2 is a maintained fork of the same package.
Ref #5605.
---
requirements/extras/consul.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/extras/consul.txt b/requirements/extras/consul.txt
index ad4ba8a08e1..7b85dde7b66 100644
--- a/requirements/extras/consul.txt
+++ b/requirements/extras/consul.txt
@@ -1 +1 @@
-python-consul
+python-consul2
From c6b37ece9c1ce086f54c5baac017d088e44a642e Mon Sep 17 00:00:00 2001
From: danthegoodman1
Date: Mon, 28 Dec 2020 18:14:11 -0500
Subject: [PATCH 107/415] Light Backends and Brokers Guides - PR from issue
#6539 (#6557)
* initial work
* move
* test
* hm
* i dont understand these urls lol
* added SQS, modified redis
* Minor fixes
* moved information to index.rst
* remove extra space
* moved and renamed
* Fix link to new backends and brokers section
Co-authored-by: Dan Goodman
Co-authored-by: Matus Valo
---
.../backends-and-brokers/index.rst | 99 +++++++++++++++++++
.../rabbitmq.rst | 0
.../redis.rst | 0
.../{brokers => backends-and-brokers}/sqs.rst | 0
docs/getting-started/brokers/index.rst | 54 ----------
docs/getting-started/index.rst | 2 +-
6 files changed, 100 insertions(+), 55 deletions(-)
create mode 100644 docs/getting-started/backends-and-brokers/index.rst
rename docs/getting-started/{brokers => backends-and-brokers}/rabbitmq.rst (100%)
rename docs/getting-started/{brokers => backends-and-brokers}/redis.rst (100%)
rename docs/getting-started/{brokers => backends-and-brokers}/sqs.rst (100%)
delete mode 100644 docs/getting-started/brokers/index.rst
diff --git a/docs/getting-started/backends-and-brokers/index.rst b/docs/getting-started/backends-and-brokers/index.rst
new file mode 100644
index 00000000000..463fdc7615c
--- /dev/null
+++ b/docs/getting-started/backends-and-brokers/index.rst
@@ -0,0 +1,99 @@
+.. _brokers:
+
+======================
+ Backends and Brokers
+======================
+
+:Release: |version|
+:Date: |today|
+
+Celery supports several message transport alternatives.
+
+.. _broker_toc:
+
+Broker Instructions
+===================
+
+.. toctree::
+ :maxdepth: 1
+
+ rabbitmq
+ redis
+ sqs
+
+.. _broker-overview:
+
+Broker Overview
+===============
+
+This is comparison table of the different transports supports,
+more information can be found in the documentation for each
+individual transport (see :ref:`broker_toc`).
+
++---------------+--------------+----------------+--------------------+
+| **Name** | **Status** | **Monitoring** | **Remote Control** |
++---------------+--------------+----------------+--------------------+
+| *RabbitMQ* | Stable | Yes | Yes |
++---------------+--------------+----------------+--------------------+
+| *Redis* | Stable | Yes | Yes |
++---------------+--------------+----------------+--------------------+
+| *Amazon SQS* | Stable | No | No |
++---------------+--------------+----------------+--------------------+
+| *Zookeeper* | Experimental | No | No |
++---------------+--------------+----------------+--------------------+
+
+Experimental brokers may be functional but they don't have
+dedicated maintainers.
+
+Missing monitor support means that the transport doesn't
+implement events, and as such Flower, `celery events`, `celerymon`
+and other event-based monitoring tools won't work.
+
+Remote control means the ability to inspect and manage workers
+at runtime using the `celery inspect` and `celery control` commands
+(and other tools using the remote control API).
+
+Summaries
+=========
+
+*Note: This section is not comprehensive of backends and brokers.*
+
+Celery has the ability to communicate and store with many different backends (Result Stores) and brokers (Message Transports).
+
+Redis
+-----
+
+Redis can be both a backend and a broker.
+
+**As a Broker:** Redis works well for rapid transport of small messages. Large messages can congest the system.
+
+:ref:`See documentation for details `
+
+**As a Backend:** Redis is a super fast K/V store, making it very efficient for fetching the results of a task call. As with the design of Redis, you do have to consider the limit memory available to store your data, and how you handle data persistence. If result persistence is important, consider using another DB for your backend.
+
+RabbitMQ
+--------
+
+RabbitMQ is a broker.
+
+RabbitMQ handles larger messages better than Redis, however if many messages are coming in very quickly, scaling can become a concern and Redis or SQS should be considered unless RabbitMQ is running at very large scale.
+
+:ref:`See documentation for details `
+
+*Note: RabbitMQ (as the broker) and Redis (as the backend) are very commonly used together. If more guaranteed long-term persistence is needed from the result store, consider using PostgreSQL or MySQL (through SQLAlchemy), Cassandra, or a custom defined backend.*
+
+SQS
+---
+
+SQS is a broker.
+
+If you already integrate tightly with AWS, and are familiar with SQS, it presents a great option as a broker. It is extremely scalable and completely managed, and manages task delegation similarly to RabbitMQ. It does lack some of the features of the RabbitMQ broker such as ``worker remote control commands``.
+
+:ref:`See documentation for details `
+
+SQLAlchemy
+----------
+
+SQLAlchemy is backend.
+
+It allows Celery to interface with MySQL, PostgreSQL, SQlite, and more. It is a ORM, and is the way Celery can use a SQL DB as a result backend. Historically, SQLAlchemy has not been the most stable result backend so if chosen one should proceed with caution.
diff --git a/docs/getting-started/brokers/rabbitmq.rst b/docs/getting-started/backends-and-brokers/rabbitmq.rst
similarity index 100%
rename from docs/getting-started/brokers/rabbitmq.rst
rename to docs/getting-started/backends-and-brokers/rabbitmq.rst
diff --git a/docs/getting-started/brokers/redis.rst b/docs/getting-started/backends-and-brokers/redis.rst
similarity index 100%
rename from docs/getting-started/brokers/redis.rst
rename to docs/getting-started/backends-and-brokers/redis.rst
diff --git a/docs/getting-started/brokers/sqs.rst b/docs/getting-started/backends-and-brokers/sqs.rst
similarity index 100%
rename from docs/getting-started/brokers/sqs.rst
rename to docs/getting-started/backends-and-brokers/sqs.rst
diff --git a/docs/getting-started/brokers/index.rst b/docs/getting-started/brokers/index.rst
deleted file mode 100644
index 0a2b6a78741..00000000000
--- a/docs/getting-started/brokers/index.rst
+++ /dev/null
@@ -1,54 +0,0 @@
-.. _brokers:
-
-=====================
- Brokers
-=====================
-
-:Release: |version|
-:Date: |today|
-
-Celery supports several message transport alternatives.
-
-.. _broker_toc:
-
-Broker Instructions
-===================
-
-.. toctree::
- :maxdepth: 1
-
- rabbitmq
- redis
- sqs
-
-.. _broker-overview:
-
-Broker Overview
-===============
-
-This is comparison table of the different transports supports,
-more information can be found in the documentation for each
-individual transport (see :ref:`broker_toc`).
-
-+---------------+--------------+----------------+--------------------+
-| **Name** | **Status** | **Monitoring** | **Remote Control** |
-+---------------+--------------+----------------+--------------------+
-| *RabbitMQ* | Stable | Yes | Yes |
-+---------------+--------------+----------------+--------------------+
-| *Redis* | Stable | Yes | Yes |
-+---------------+--------------+----------------+--------------------+
-| *Amazon SQS* | Stable | No | No |
-+---------------+--------------+----------------+--------------------+
-| *Zookeeper* | Experimental | No | No |
-+---------------+--------------+----------------+--------------------+
-
-Experimental brokers may be functional but they don't have
-dedicated maintainers.
-
-Missing monitor support means that the transport doesn't
-implement events, and as such Flower, `celery events`, `celerymon`
-and other event-based monitoring tools won't work.
-
-Remote control means the ability to inspect and manage workers
-at runtime using the `celery inspect` and `celery control` commands
-(and other tools using the remote control API).
diff --git a/docs/getting-started/index.rst b/docs/getting-started/index.rst
index b590a18d53d..083ccb026f7 100644
--- a/docs/getting-started/index.rst
+++ b/docs/getting-started/index.rst
@@ -9,7 +9,7 @@
:maxdepth: 2
introduction
- brokers/index
+ backends-and-brokers/index
first-steps-with-celery
next-steps
resources
From 1afe22daab2978ce4ea7269e7de3b9f5c0e20d34 Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Sun, 20 Dec 2020 04:23:17 +0100
Subject: [PATCH 108/415] Mention rpc:// backend in Backend and Brokers page
---
docs/getting-started/backends-and-brokers/index.rst | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/docs/getting-started/backends-and-brokers/index.rst b/docs/getting-started/backends-and-brokers/index.rst
index 463fdc7615c..ac872b9aeed 100644
--- a/docs/getting-started/backends-and-brokers/index.rst
+++ b/docs/getting-started/backends-and-brokers/index.rst
@@ -76,10 +76,12 @@ RabbitMQ
RabbitMQ is a broker.
-RabbitMQ handles larger messages better than Redis, however if many messages are coming in very quickly, scaling can become a concern and Redis or SQS should be considered unless RabbitMQ is running at very large scale.
+**As a Broker:** RabbitMQ handles larger messages better than Redis, however if many messages are coming in very quickly, scaling can become a concern and Redis or SQS should be considered unless RabbitMQ is running at very large scale.
:ref:`See documentation for details `
+**As a Backend:** RabbitMQ can store results via ``rpc://`` backend. This backend creates separate temporary queue for each new result of a task call. Creating new queue for each task call can be a bottleneck for high volume usage.
+
*Note: RabbitMQ (as the broker) and Redis (as the backend) are very commonly used together. If more guaranteed long-term persistence is needed from the result store, consider using PostgreSQL or MySQL (through SQLAlchemy), Cassandra, or a custom defined backend.*
SQS
From 06eba1556525c497f9417540b6aa49279fcdf43f Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Sun, 20 Dec 2020 09:53:13 +0100
Subject: [PATCH 109/415] Fix information about rpc:// backend
---
docs/getting-started/backends-and-brokers/index.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/getting-started/backends-and-brokers/index.rst b/docs/getting-started/backends-and-brokers/index.rst
index ac872b9aeed..d50b0b5e526 100644
--- a/docs/getting-started/backends-and-brokers/index.rst
+++ b/docs/getting-started/backends-and-brokers/index.rst
@@ -80,7 +80,7 @@ RabbitMQ is a broker.
:ref:`See documentation for details `
-**As a Backend:** RabbitMQ can store results via ``rpc://`` backend. This backend creates separate temporary queue for each new result of a task call. Creating new queue for each task call can be a bottleneck for high volume usage.
+**As a Backend:** RabbitMQ can store results via ``rpc://`` backend. This backend creates separate temporary queue for each client.
*Note: RabbitMQ (as the broker) and Redis (as the backend) are very commonly used together. If more guaranteed long-term persistence is needed from the result store, consider using PostgreSQL or MySQL (through SQLAlchemy), Cassandra, or a custom defined backend.*
From 3546059338bea70bab7ef9d961b00c161938b15e Mon Sep 17 00:00:00 2001
From: 0xflotus <0xflotus@gmail.com>
Date: Wed, 30 Dec 2020 02:53:04 +0100
Subject: [PATCH 110/415] enabled syntax highlighting
---
README.rst | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.rst b/README.rst
index e1cdae5ee0e..3017bdf04db 100644
--- a/README.rst
+++ b/README.rst
@@ -116,7 +116,9 @@ Celery is...
It has an active, friendly community you can talk to for support,
like at our `mailing-list`_, or the IRC channel.
- Here's one of the simplest applications you can make::
+ Here's one of the simplest applications you can make:
+
+ .. code-block:: python
from celery import Celery
From 2dd6769d1f24d4af8a7edb66f8de9f0f6ee1c371 Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Tue, 5 Jan 2021 03:06:38 +0600
Subject: [PATCH 111/415] WIP - initial github action migrations (#6547)
* initial github action migrations
* add more settings
* add github-actions block to tox.ini
* apt packages install block
* apt packages install block force
* rename py env list
---
.github/workflows/python-package.yml | 55 ++++++++++++++++++++++++++++
tox.ini | 8 ++++
2 files changed, 63 insertions(+)
create mode 100644 .github/workflows/python-package.yml
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
new file mode 100644
index 00000000000..190cf18ad54
--- /dev/null
+++ b/.github/workflows/python-package.yml
@@ -0,0 +1,55 @@
+# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Celery
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ['3.6', '3.7', '3.8', '3.9', 'pypy3']
+
+ steps:
+ - name: Install apt packages
+ run: |
+ sudo apt-get install -f libcurl4-openssl-dev libssl-dev gnutls-dev httping expect
+ - uses: actions/checkout@v2
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Get pip cache dir
+ id: pip-cache
+ run: |
+ echo "::set-output name=dir::$(pip cache dir)"
+ - name: Cache
+ uses: actions/cache@v2
+ with:
+ path: ${{ steps.pip-cache.outputs.dir }}
+ key:
+ ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
+ restore-keys: |
+ ${{ matrix.python-version }}-v1-
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip tox tox-gh-actions
+ python -m pip install flake8 pytest
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+ - name: Lint with flake8
+ run: |
+ # stop the build if there are Python syntax errors or undefined names
+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+ flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+ - name: Run Tox
+ run: |
+ tox -v
diff --git a/tox.ini b/tox.ini
index efdfa1c56be..2196d3d8d47 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,6 +8,14 @@ envlist =
configcheck
bandit
+[gh-actions]
+python =
+ 3.6: 3.6
+ 3.7: 3.7
+ 3.8: 3.8
+ 3.9: 3.9
+ pypy3: pypy3
+
[testenv]
deps=
-r{toxinidir}/requirements/default.txt
From 3111feb9e5b279ec066235b4a5225180aedb20d0 Mon Sep 17 00:00:00 2001
From: Felix Yan
Date: Thu, 7 Jan 2021 07:02:48 +0800
Subject: [PATCH 112/415] Correct a typo in multi.py
---
celery/bin/multi.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/celery/bin/multi.py b/celery/bin/multi.py
index 82a86a6129e..3a9e026b88a 100644
--- a/celery/bin/multi.py
+++ b/celery/bin/multi.py
@@ -67,7 +67,7 @@
$ celery multi show 10 -l INFO -Q:1-3 images,video -Q:4,5 data
-Q default -L:4,5 DEBUG
- $ # Additional options are added to each celery worker' comamnd,
+ $ # Additional options are added to each celery worker's command,
$ # but you can also modify the options for ranges of, or specific workers
$ # 3 workers: Two with 3 processes, and one with 10 processes.
From f460b42108d80c7f68884be3e953838bfcf5715f Mon Sep 17 00:00:00 2001
From: tumb1er
Date: Tue, 5 Jan 2021 10:39:52 +0300
Subject: [PATCH 113/415] Uncomment couchbase requirements in ci
---
requirements/test-ci-default.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/test-ci-default.txt b/requirements/test-ci-default.txt
index fdcf4684733..953ed9aecc7 100644
--- a/requirements/test-ci-default.txt
+++ b/requirements/test-ci-default.txt
@@ -12,7 +12,7 @@
-r extras/thread.txt
-r extras/elasticsearch.txt
-r extras/couchdb.txt
-#-r extras/couchbase.txt
+-r extras/couchbase.txt
-r extras/arangodb.txt
-r extras/consul.txt
-r extras/cosmosdbsql.txt
From 7d6c9e4a397f532da5e6c9f72c163af1a9f8cc90 Mon Sep 17 00:00:00 2001
From: tumb1er
Date: Tue, 5 Jan 2021 10:40:07 +0300
Subject: [PATCH 114/415] Use result_chord_join_timeout instead of hardcoded
default value
---
celery/backends/base.py | 4 +++-
celery/backends/redis.py | 5 ++++-
t/unit/backends/test_base.py | 12 ++++++++++++
t/unit/backends/test_redis.py | 15 +++++++++++++++
4 files changed, 34 insertions(+), 2 deletions(-)
diff --git a/celery/backends/base.py b/celery/backends/base.py
index 1aac2a0fc95..22fe0c79cb9 100644
--- a/celery/backends/base.py
+++ b/celery/backends/base.py
@@ -928,7 +928,9 @@ def on_chord_part_return(self, request, state, result, **kwargs):
j = deps.join_native if deps.supports_native_join else deps.join
try:
with allow_join_result():
- ret = j(timeout=3.0, propagate=True)
+ ret = j(
+ timeout=app.conf.result_chord_join_timeout,
+ propagate=True)
except Exception as exc: # pylint: disable=broad-except
try:
culprit = next(deps._failed_join_report())
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index dd3677f569c..e767de05c58 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -469,7 +469,10 @@ def on_chord_part_return(self, request, state, result,
else header_result.join
)
with allow_join_result():
- resl = join_func(timeout=3.0, propagate=True)
+ resl = join_func(
+ timeout=app.conf.result_chord_join_timeout,
+ propagate=True
+ )
else:
# Otherwise simply extract and decode the results we
# stashed along the way, which should be faster for large
diff --git a/t/unit/backends/test_base.py b/t/unit/backends/test_base.py
index 0e4bb133c85..6f54bdf37f1 100644
--- a/t/unit/backends/test_base.py
+++ b/t/unit/backends/test_base.py
@@ -786,6 +786,18 @@ def callback(result):
callback.backend.fail_from_current_stack = Mock()
yield task, deps, cb
+ def test_chord_part_return_timeout(self):
+ with self._chord_part_context(self.b) as (task, deps, _):
+ try:
+ self.app.conf.result_chord_join_timeout += 1.0
+ self.b.on_chord_part_return(task.request, 'SUCCESS', 10)
+ finally:
+ self.app.conf.result_chord_join_timeout -= 1.0
+
+ self.b.expire.assert_not_called()
+ deps.delete.assert_called_with()
+ deps.join_native.assert_called_with(propagate=True, timeout=4.0)
+
def test_chord_part_return_propagate_set(self):
with self._chord_part_context(self.b) as (task, deps, _):
self.b.on_chord_part_return(task.request, 'SUCCESS', 10)
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index f534077a4fd..445a9bb10e7 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -1012,6 +1012,21 @@ def test_apply_chord_complex_header(self):
mock_header_result.save.assert_called_once_with(backend=self.b)
mock_header_result.save.reset_mock()
+ def test_on_chord_part_return_timeout(self, complex_header_result):
+ tasks = [self.create_task(i) for i in range(10)]
+ random.shuffle(tasks)
+ try:
+ self.app.conf.result_chord_join_timeout += 1.0
+ for task, result_val in zip(tasks, itertools.cycle((42, ))):
+ self.b.on_chord_part_return(
+ task.request, states.SUCCESS, result_val,
+ )
+ finally:
+ self.app.conf.result_chord_join_timeout -= 1.0
+
+ join_func = complex_header_result.return_value.join_native
+ join_func.assert_called_once_with(timeout=4.0, propagate=True)
+
@pytest.mark.parametrize("supports_native_join", (True, False))
def test_on_chord_part_return(
self, complex_header_result, supports_native_join,
From 79a65d17284908bba3380840e2ac017c5bb27308 Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Fri, 18 Dec 2020 00:22:40 +0100
Subject: [PATCH 115/415] Added integration tests for control.inspect()
---
t/integration/test_inspect.py | 229 ++++++++++++++++++++++++++++++++++
1 file changed, 229 insertions(+)
create mode 100644 t/integration/test_inspect.py
diff --git a/t/integration/test_inspect.py b/t/integration/test_inspect.py
new file mode 100644
index 00000000000..af5cd7dcfd6
--- /dev/null
+++ b/t/integration/test_inspect.py
@@ -0,0 +1,229 @@
+import os
+from datetime import datetime, timedelta
+from unittest.mock import ANY
+from time import sleep
+
+import pytest
+
+from celery.utils.nodenames import anon_nodename
+
+from .tasks import sleeping, add
+
+NODENAME = anon_nodename()
+
+_flaky = pytest.mark.flaky(reruns=5, reruns_delay=2)
+_timeout = pytest.mark.timeout(timeout=300)
+
+
+def flaky(fn):
+ return _timeout(_flaky(fn))
+
+
+@pytest.fixture()
+def inspect(manager):
+ return manager.app.control.inspect()
+
+
+class test_Inspect:
+ """Integration tests fo app.control.inspect() API"""
+
+ @flaky
+ def test_ping(self, inspect):
+ """Tests pinging the worker"""
+ ret = inspect.ping()
+ assert len(ret) == 1
+ assert ret[NODENAME] == {'ok': 'pong'}
+ # TODO: Check ping() is returning None after stopping worker.
+ # This is tricky since current test suite does not support stopping of
+ # the worker.
+
+ @flaky
+ def test_clock(self, inspect):
+ """Tests getting clock information from worker"""
+ ret = inspect.clock()
+ assert len(ret) == 1
+ assert ret[NODENAME]['clock'] > 0
+
+ @flaky
+ def test_registered(self, inspect):
+ """Tests listing registered tasks"""
+ ret = inspect.registered()
+ assert len(ret) == 1
+ # TODO: We can check also the values of the registered methods
+ len(ret[NODENAME]) > 0
+
+ @flaky
+ def test_active_queues(self, inspect):
+ """Tests listing active queues"""
+ ret = inspect.active_queues()
+ assert len(ret) == 1
+ assert ret[NODENAME] == [
+ {
+ 'alias': None,
+ 'auto_delete': False,
+ 'binding_arguments': None,
+ 'bindings': [],
+ 'consumer_arguments': None,
+ 'durable': True,
+ 'exchange': {
+ 'arguments': None,
+ 'auto_delete': False,
+ 'delivery_mode': None,
+ 'durable': True,
+ 'name': 'celery',
+ 'no_declare': False,
+ 'passive': False,
+ 'type': 'direct'
+ },
+ 'exclusive': False,
+ 'expires': None,
+ 'max_length': None,
+ 'max_length_bytes': None,
+ 'max_priority': None,
+ 'message_ttl': None,
+ 'name': 'celery',
+ 'no_ack': False,
+ 'no_declare': None,
+ 'queue_arguments': None,
+ 'routing_key': 'celery'}
+ ]
+
+ @flaky
+ def test_active(self, inspect):
+ """Tests listing active tasks"""
+ res = sleeping.delay(5)
+ sleep(1)
+ ret = inspect.active()
+ assert len(ret) == 1
+ assert ret[NODENAME] == [
+ {
+ 'id': res.task_id,
+ 'name': 't.integration.tasks.sleeping',
+ 'args': [5],
+ 'kwargs': {},
+ 'type': 't.integration.tasks.sleeping',
+ 'hostname': ANY,
+ 'time_start': ANY,
+ 'acknowledged': True,
+ 'delivery_info': {
+ 'exchange': '',
+ 'routing_key': 'celery',
+ 'priority': 0,
+ 'redelivered': False
+ },
+ 'worker_pid': ANY
+ }
+ ]
+
+ @flaky
+ def test_scheduled(self, inspect):
+ """Tests listing scheduled tasks"""
+ exec_time = datetime.utcnow() + timedelta(seconds=5)
+ res = add.apply_async([1, 2], {'z': 3}, eta=exec_time)
+ ret = inspect.scheduled()
+ assert len(ret) == 1
+ assert ret[NODENAME] == [
+ {
+ 'eta': exec_time.strftime('%Y-%m-%dT%H:%M:%S.%f') + '+00:00',
+ 'priority': 6,
+ 'request': {
+ 'id': res.task_id,
+ 'name': 't.integration.tasks.add',
+ 'args': [1, 2],
+ 'kwargs': {'z': 3},
+ 'type': 't.integration.tasks.add',
+ 'hostname': ANY,
+ 'time_start': None,
+ 'acknowledged': False,
+ 'delivery_info': {
+ 'exchange': '',
+ 'routing_key': 'celery',
+ 'priority': 0,
+ 'redelivered': False
+ },
+ 'worker_pid': None
+ }
+ }
+ ]
+
+ @flaky
+ def test_query_task(self, inspect):
+ """Task that does not exist or is finished"""
+ ret = inspect.query_task('d08b257e-a7f1-4b92-9fea-be911441cb2a')
+ assert len(ret) == 1
+ assert ret[NODENAME] == {}
+
+ # Task in progress
+ res = sleeping.delay(5)
+ sleep(1)
+ ret = inspect.query_task(res.task_id)
+ assert len(ret) == 1
+ assert ret[NODENAME] == {
+ res.task_id: [
+ 'active', {
+ 'id': res.task_id,
+ 'name': 't.integration.tasks.sleeping',
+ 'args': [5],
+ 'kwargs': {},
+ 'type': 't.integration.tasks.sleeping',
+ 'hostname': NODENAME,
+ 'time_start': ANY,
+ 'acknowledged': True,
+ 'delivery_info': {
+ 'exchange': '',
+ 'routing_key': 'celery',
+ 'priority': 0,
+ 'redelivered': False
+ },
+ # worker is running in the same process as separate thread
+ 'worker_pid': ANY
+ }
+ ]
+ }
+
+ @flaky
+ def test_stats(self, inspect):
+ """tests fetching statistics"""
+ ret = inspect.stats()
+ assert len(ret) == 1
+ assert ret[NODENAME]['pool']['max-concurrency'] == 1
+ assert len(ret[NODENAME]['pool']['processes']) == 1
+ assert ret[NODENAME]['uptime'] > 0
+ # worker is running in the same process as separate thread
+ assert ret[NODENAME]['pid'] == os.getpid()
+
+ @flaky
+ def test_report(self, inspect):
+ """Tests fetching report"""
+ ret = inspect.report()
+ assert len(ret) == 1
+ assert ret[NODENAME] == {'ok': ANY}
+
+ @flaky
+ def test_revoked(self, inspect):
+ """Testing revoking of task"""
+ # Fill the queue with tasks to fill the queue
+ for _ in range(4):
+ sleeping.delay(2)
+ # Execute task and revoke it
+ result = add.apply_async((1, 1))
+ result.revoke()
+ ret = inspect.revoked()
+ assert len(ret) == 1
+ assert result.task_id in ret[NODENAME]
+
+ @flaky
+ def test_conf(self, inspect):
+ """Tests getting configuration"""
+ ret = inspect.conf()
+ assert len(ret) == 1
+ assert ret[NODENAME]['worker_hijack_root_logger'] == ANY
+ assert ret[NODENAME]['worker_log_color'] == ANY
+ assert ret[NODENAME]['accept_content'] == ANY
+ assert ret[NODENAME]['enable_utc'] == ANY
+ assert ret[NODENAME]['timezone'] == ANY
+ assert ret[NODENAME]['broker_url'] == ANY
+ assert ret[NODENAME]['result_backend'] == ANY
+ assert ret[NODENAME]['broker_heartbeat'] == ANY
+ assert ret[NODENAME]['deprecated_settings'] == ANY
+ assert ret[NODENAME]['include'] == ANY
From 586c69fd23159f6b73aaa5e85352248dab601047 Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Mon, 21 Dec 2020 00:36:48 +0100
Subject: [PATCH 116/415] Added integration test for revoking a task
---
t/integration/test_tasks.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/t/integration/test_tasks.py b/t/integration/test_tasks.py
index ba5f4fbba77..17d59f9851d 100644
--- a/t/integration/test_tasks.py
+++ b/t/integration/test_tasks.py
@@ -175,6 +175,22 @@ def test_fail(self, manager):
assert result.failed() is True
assert result.successful() is False
+ @flaky
+ def test_revoked(self, manager):
+ """Testing revoking of task"""
+ # Fill the queue with tasks to fill the queue
+ for _ in range(4):
+ sleeping.delay(2)
+ # Execute task and revoke it
+ result = add.apply_async((1, 1))
+ result.revoke()
+ with pytest.raises(celery.exceptions.TaskRevokedError):
+ result.get()
+ assert result.status == 'REVOKED'
+ assert result.ready() is True
+ assert result.failed() is False
+ assert result.successful() is False
+
@flaky
def test_wrong_arguments(self, manager):
"""Tests that proper exceptions are raised when task is called with wrong arguments."""
From 2c6f46d4a61a8256bdc4f6ef348dabf7011ccd9a Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Mon, 21 Dec 2020 13:21:47 +0100
Subject: [PATCH 117/415] Improve test_registered integration test
---
t/integration/test_inspect.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/t/integration/test_inspect.py b/t/integration/test_inspect.py
index af5cd7dcfd6..49275622aa2 100644
--- a/t/integration/test_inspect.py
+++ b/t/integration/test_inspect.py
@@ -1,4 +1,5 @@
import os
+import re
from datetime import datetime, timedelta
from unittest.mock import ANY
from time import sleep
@@ -47,10 +48,17 @@ def test_clock(self, inspect):
@flaky
def test_registered(self, inspect):
"""Tests listing registered tasks"""
+ # TODO: We can check also the exact values of the registered methods
ret = inspect.registered()
assert len(ret) == 1
- # TODO: We can check also the values of the registered methods
len(ret[NODENAME]) > 0
+ for task_name in ret[NODENAME]:
+ assert isinstance(task_name, str)
+
+ ret = inspect.registered('name')
+ for task_info in ret[NODENAME]:
+ # task_info is in form 'TASK_NAME [name=TASK_NAME]'
+ assert re.fullmatch(r'\S+ \[name=\S+\]', task_info)
@flaky
def test_active_queues(self, inspect):
From f6ca74558a3afe8ea0038dd6b480844ce45958f6 Mon Sep 17 00:00:00 2001
From: JanusAsmussen <46710143+JanusAsmussen@users.noreply.github.com>
Date: Sat, 9 Jan 2021 15:44:53 +0100
Subject: [PATCH 118/415] Upgrade AzureBlockBlob storage backend to use Azure
blob storage library v12 (#6580)
* Upgrade AzureBlockBlob backend to use library azure-storage-blob v12
* Fix minor bug in AzureBlockBlob backend unit test
* Upgrade AzureBlockBlob backend to use library azure-storage-blob v12
* Fix minor bug in AzureBlockBlob backend unit test
* Bug fixes in AzureBlockBlob class and unit tests
* Update docker-compose.yml to use Microsoft's official azurite docker image
Co-authored-by: Janus Asmussen
---
celery/backends/azureblockblob.py | 92 +++++++++++---------------
docker/docker-compose.yml | 2 +-
requirements/extras/azureblockblob.txt | 4 +-
t/unit/backends/test_azureblockblob.py | 60 ++++++++++-------
4 files changed, 78 insertions(+), 80 deletions(-)
diff --git a/celery/backends/azureblockblob.py b/celery/backends/azureblockblob.py
index f287200dcc7..93ff600a23d 100644
--- a/celery/backends/azureblockblob.py
+++ b/celery/backends/azureblockblob.py
@@ -8,13 +8,11 @@
from .base import KeyValueStoreBackend
try:
- from azure import storage as azurestorage
- from azure.common import AzureMissingResourceHttpError
- from azure.storage.blob import BlockBlobService
- from azure.storage.common.retry import ExponentialRetry
-except ImportError: # pragma: no cover
- azurestorage = BlockBlobService = ExponentialRetry = \
- AzureMissingResourceHttpError = None # noqa
+ import azure.storage.blob as azurestorage
+ from azure.storage.blob import BlobServiceClient
+ from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
+except ImportError:
+ azurestorage = None
__all__ = ("AzureBlockBlobBackend",)
@@ -27,17 +25,14 @@ class AzureBlockBlobBackend(KeyValueStoreBackend):
def __init__(self,
url=None,
container_name=None,
- retry_initial_backoff_sec=None,
- retry_increment_base=None,
- retry_max_attempts=None,
*args,
**kwargs):
super().__init__(*args, **kwargs)
- if azurestorage is None:
+ if azurestorage is None or azurestorage.__version__ < '12':
raise ImproperlyConfigured(
- "You need to install the azure-storage library to use the "
- "AzureBlockBlob backend")
+ "You need to install the azure-storage-blob v12 library to"
+ "use the AzureBlockBlob backend")
conf = self.app.conf
@@ -47,18 +42,6 @@ def __init__(self,
container_name or
conf["azureblockblob_container_name"])
- self._retry_initial_backoff_sec = (
- retry_initial_backoff_sec or
- conf["azureblockblob_retry_initial_backoff_sec"])
-
- self._retry_increment_base = (
- retry_increment_base or
- conf["azureblockblob_retry_increment_base"])
-
- self._retry_max_attempts = (
- retry_max_attempts or
- conf["azureblockblob_retry_max_attempts"])
-
@classmethod
def _parse_url(cls, url, prefix="azureblockblob://"):
connection_string = url[len(prefix):]
@@ -68,26 +51,22 @@ def _parse_url(cls, url, prefix="azureblockblob://"):
return connection_string
@cached_property
- def _client(self):
- """Return the Azure Storage Block Blob service.
+ def _blob_service_client(self):
+ """Return the Azure Storage Blob service client.
If this is the first call to the property, the client is created and
the container is created if it doesn't yet exist.
"""
- client = BlockBlobService(connection_string=self._connection_string)
-
- created = client.create_container(
- container_name=self._container_name, fail_on_exist=False)
-
- if created:
- LOGGER.info("Created Azure Blob Storage container %s",
- self._container_name)
+ client = BlobServiceClient.from_connection_string(self._connection_string)
- client.retry = ExponentialRetry(
- initial_backoff=self._retry_initial_backoff_sec,
- increment_base=self._retry_increment_base,
- max_attempts=self._retry_max_attempts).retry
+ try:
+ client.create_container(name=self._container_name)
+ msg = f"Container created with name {self._container_name}."
+ except ResourceExistsError:
+ msg = f"Container with name {self._container_name} already." \
+ "exists. This will not be created."
+ LOGGER.info(msg)
return client
@@ -96,16 +75,18 @@ def get(self, key):
Args:
key: The key for which to read the value.
-
"""
key = bytes_to_str(key)
- LOGGER.debug("Getting Azure Block Blob %s/%s",
- self._container_name, key)
+ LOGGER.debug("Getting Azure Block Blob %s/%s", self._container_name, key)
+
+ blob_client = self._blob_service_client.get_blob_client(
+ container=self._container_name,
+ blob=key,
+ )
try:
- return self._client.get_blob_to_text(
- self._container_name, key).content
- except AzureMissingResourceHttpError:
+ return blob_client.download_blob().readall().decode()
+ except ResourceNotFoundError:
return None
def set(self, key, value):
@@ -117,11 +98,14 @@ def set(self, key, value):
"""
key = bytes_to_str(key)
- LOGGER.debug("Creating Azure Block Blob at %s/%s",
- self._container_name, key)
+ LOGGER.debug(f"Creating azure blob at {self._container_name}/{key}")
- return self._client.create_blob_from_text(
- self._container_name, key, value)
+ blob_client = self._blob_service_client.get_blob_client(
+ container=self._container_name,
+ blob=key,
+ )
+
+ blob_client.upload_blob(value, overwrite=True)
def mget(self, keys):
"""Read all the values for the provided keys.
@@ -140,7 +124,11 @@ def delete(self, key):
"""
key = bytes_to_str(key)
- LOGGER.debug("Deleting Azure Block Blob at %s/%s",
- self._container_name, key)
+ LOGGER.debug(f"Deleting azure blob at {self._container_name}/{key}")
+
+ blob_client = self._blob_service_client.get_blob_client(
+ container=self._container_name,
+ blob=key,
+ )
- self._client.delete_blob(self._container_name, key)
+ blob_client.delete_blob()
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 428fe204475..d0c4c34179e 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -36,4 +36,4 @@ services:
image: dwmkerr/dynamodb:38
azurite:
- image: arafato/azurite:2.6.5
+ image: mcr.microsoft.com/azure-storage/azurite:3.10.0
diff --git a/requirements/extras/azureblockblob.txt b/requirements/extras/azureblockblob.txt
index 37c66507d89..e533edb7e76 100644
--- a/requirements/extras/azureblockblob.txt
+++ b/requirements/extras/azureblockblob.txt
@@ -1,3 +1 @@
-azure-storage==0.36.0
-azure-common==1.1.5
-azure-storage-common==1.1.0
+azure-storage-blob==12.6.0
diff --git a/t/unit/backends/test_azureblockblob.py b/t/unit/backends/test_azureblockblob.py
index 969993290d4..596764bc174 100644
--- a/t/unit/backends/test_azureblockblob.py
+++ b/t/unit/backends/test_azureblockblob.py
@@ -41,55 +41,67 @@ def test_bad_connection_url(self):
with pytest.raises(ImproperlyConfigured):
AzureBlockBlobBackend._parse_url("")
- @patch(MODULE_TO_MOCK + ".BlockBlobService")
+ @patch(MODULE_TO_MOCK + ".BlobServiceClient")
def test_create_client(self, mock_blob_service_factory):
- mock_blob_service_instance = Mock()
- mock_blob_service_factory.return_value = mock_blob_service_instance
+ mock_blob_service_client_instance = Mock()
+ mock_blob_service_factory.from_connection_string.return_value = mock_blob_service_client_instance
backend = AzureBlockBlobBackend(app=self.app, url=self.url)
# ensure container gets created on client access...
- assert mock_blob_service_instance.create_container.call_count == 0
- assert backend._client is not None
- assert mock_blob_service_instance.create_container.call_count == 1
+ assert mock_blob_service_client_instance.create_container.call_count == 0
+ assert backend._blob_service_client is not None
+ assert mock_blob_service_client_instance.create_container.call_count == 1
# ...but only once per backend instance
- assert backend._client is not None
- assert mock_blob_service_instance.create_container.call_count == 1
+ assert backend._blob_service_client is not None
+ assert mock_blob_service_client_instance.create_container.call_count == 1
- @patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._client")
+ @patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._blob_service_client")
def test_get(self, mock_client):
self.backend.get(b"mykey")
- mock_client.get_blob_to_text.assert_called_once_with(
- "celery", "mykey")
+ mock_client.get_blob_client \
+ .assert_called_once_with(blob="mykey", container="celery")
- @patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._client")
+ mock_client.get_blob_client.return_value \
+ .download_blob.return_value \
+ .readall.return_value \
+ .decode.assert_called_once()
+
+ @patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._blob_service_client")
def test_get_missing(self, mock_client):
- mock_client.get_blob_to_text.side_effect = \
- azureblockblob.AzureMissingResourceHttpError("Missing", 404)
+ mock_client.get_blob_client.return_value \
+ .download_blob.return_value \
+ .readall.side_effect = azureblockblob.ResourceNotFoundError
assert self.backend.get(b"mykey") is None
- @patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._client")
+ @patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._blob_service_client")
def test_set(self, mock_client):
self.backend._set_with_state(b"mykey", "myvalue", states.SUCCESS)
- mock_client.create_blob_from_text.assert_called_once_with(
- "celery", "mykey", "myvalue")
+ mock_client.get_blob_client.assert_called_once_with(
+ container="celery", blob="mykey")
+
+ mock_client.get_blob_client.return_value \
+ .upload_blob.assert_called_once_with("myvalue", overwrite=True)
- @patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._client")
+ @patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._blob_service_client")
def test_mget(self, mock_client):
keys = [b"mykey1", b"mykey2"]
self.backend.mget(keys)
- mock_client.get_blob_to_text.assert_has_calls(
- [call("celery", "mykey1"),
- call("celery", "mykey2")])
+ mock_client.get_blob_client.assert_has_calls(
+ [call(blob=key.decode(), container='celery') for key in keys],
+ any_order=True,)
- @patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._client")
+ @patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._blob_service_client")
def test_delete(self, mock_client):
self.backend.delete(b"mykey")
- mock_client.delete_blob.assert_called_once_with(
- "celery", "mykey")
+ mock_client.get_blob_client.assert_called_once_with(
+ container="celery", blob="mykey")
+
+ mock_client.get_blob_client.return_value \
+ .delete_blob.assert_called_once()
From 8ff578f3e1e39475096a83904d302982fe998b9d Mon Sep 17 00:00:00 2001
From: Jorrit
Date: Sun, 10 Jan 2021 14:23:05 +0100
Subject: [PATCH 119/415] pass_context for handle_preload_options decorator
(#6583)
handle_reload_options requires the ctx argument.
---
celery/bin/graph.py | 3 ++-
celery/bin/list.py | 3 ++-
celery/bin/logtool.py | 3 ++-
celery/bin/upgrade.py | 3 ++-
4 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/celery/bin/graph.py b/celery/bin/graph.py
index 93b01e808fa..60218335d61 100644
--- a/celery/bin/graph.py
+++ b/celery/bin/graph.py
@@ -9,8 +9,9 @@
@click.group()
+@click.pass_context
@handle_preload_options
-def graph():
+def graph(ctx):
"""The ``celery graph`` command."""
diff --git a/celery/bin/list.py b/celery/bin/list.py
index 06c4fbf28bf..f170e627223 100644
--- a/celery/bin/list.py
+++ b/celery/bin/list.py
@@ -5,8 +5,9 @@
@click.group(name="list")
+@click.pass_context
@handle_preload_options
-def list_():
+def list_(ctx):
"""Get info from broker.
Note:
diff --git a/celery/bin/logtool.py b/celery/bin/logtool.py
index 83e8064bdb0..ae64c3e473f 100644
--- a/celery/bin/logtool.py
+++ b/celery/bin/logtool.py
@@ -111,8 +111,9 @@ def report(self):
@click.group()
+@click.pass_context
@handle_preload_options
-def logtool():
+def logtool(ctx):
"""The ``celery logtool`` command."""
diff --git a/celery/bin/upgrade.py b/celery/bin/upgrade.py
index e083995b674..cd9a695b702 100644
--- a/celery/bin/upgrade.py
+++ b/celery/bin/upgrade.py
@@ -11,8 +11,9 @@
@click.group()
+@click.pass_context
@handle_preload_options
-def upgrade():
+def upgrade(ctx):
"""Perform upgrade between versions."""
From 8ce3badd59e183592596f43a2215c4bb41193a5f Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Mon, 11 Jan 2021 20:07:24 +0600
Subject: [PATCH 120/415] Docker update (#6586)
* update dockerfile
* update conflicting requirements
* update conflicting requirements
* update to pypy3 & pytest requirements
* update python versions in latest contribution docs
---
CONTRIBUTING.rst | 15 +++++++--------
docker/Dockerfile | 30 +++++++++---------------------
docker/scripts/install-pyenv.sh | 8 +++-----
requirements/dev.txt | 4 ++--
requirements/test.txt | 2 +-
5 files changed, 22 insertions(+), 37 deletions(-)
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index e869a4f45fe..12a2aec700d 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -292,8 +292,7 @@ Branches
Current active version branches:
* dev (which git calls "master") (https://github.com/celery/celery/tree/master)
-* 4.2 (https://github.com/celery/celery/tree/4.2)
-* 4.1 (https://github.com/celery/celery/tree/4.1)
+* 4.5 (https://github.com/celery/celery/tree/v4.5)
* 3.1 (https://github.com/celery/celery/tree/3.1)
You can see the state of any branch by looking at the Changelog:
@@ -494,18 +493,18 @@ Some useful commands to run:
**Note:** This command will run tests for every environment defined in :file:`tox.ini`.
It takes a while.
-* ``pyenv exec python{2.7,3.5,3.6,3.7,3.8} -m pytest t/unit``
+* ``pyenv exec python{3.6,3.7,3.8} -m pytest t/unit``
To run unit tests using pytest.
- **Note:** ``{2.7,3.5,3.6,3.7,3.8}`` means you can use any of those options.
+ **Note:** ``{3.6,3.7,3.8}`` means you can use any of those options.
e.g. ``pyenv exec python3.6 -m pytest t/unit``
-* ``pyenv exec python{2.7,3.5,3.6,3.7,3.8} -m pytest t/integration``
+* ``pyenv exec python{3.6,3.7,3.8} -m pytest t/integration``
To run integration tests using pytest
- **Note:** ``{2.7,3.5,3.6,3.7,3.8}`` means you can use any of those options.
+ **Note:** ``{3.6,3.7,3.8}`` means you can use any of those options.
e.g. ``pyenv exec python3.6 -m pytest t/unit``
By default, docker-compose will mount the Celery and test folders in the Docker
@@ -516,7 +515,7 @@ use are also defined in the :file:`docker/docker-compose.yml` file.
By running ``docker-compose build celery`` an image will be created with the
name ``celery/celery:dev``. This docker image has every dependency needed
for development installed. ``pyenv`` is used to install multiple python
-versions, the docker image offers python 2.7, 3.5, 3.6, 3.7 and 3.8.
+versions, the docker image offers python 3.6, 3.7 and 3.8.
The default python version is set to 3.8.
The :file:`docker-compose.yml` file defines the necessary environment variables
@@ -677,7 +676,7 @@ Use the ``tox -e`` option if you only want to test specific Python versions:
.. code-block:: console
- $ tox -e 2.7
+ $ tox -e 3.7
Building the documentation
--------------------------
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 403052787f8..469022f0446 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,5 +1,6 @@
-FROM ubuntu:bionic
+FROM ubuntu:focal
+ENV PYTHONUNBUFFERED 1
ENV PYTHONIOENCODING UTF-8
ARG DEBIAN_FRONTEND=noninteractive
@@ -22,7 +23,8 @@ RUN apt-get update && apt-get install -y build-essential \
libncurses5-dev \
libsqlite3-dev \
wget \
- pypy \
+ pypy3 \
+ pypy3-lib \
python-openssl \
libncursesw5-dev \
zlib1g-dev \
@@ -44,10 +46,10 @@ ENV PATH="$HOME/.pyenv/bin:$PATH"
# Copy and run setup scripts
WORKDIR $PROVISIONING
-COPY docker/scripts/install-couchbase.sh .
+#COPY docker/scripts/install-couchbase.sh .
# Scripts will lose thier executable flags on copy. To avoid the extra instructions
# we call the shell directly.
-RUN sh install-couchbase.sh
+#RUN sh install-couchbase.sh
COPY docker/scripts/create-linux-user.sh .
RUN sh create-linux-user.sh
@@ -64,11 +66,9 @@ COPY --chown=1000:1000 docker/entrypoint /entrypoint
RUN chmod gu+x /entrypoint
# Define the local pyenvs
-RUN pyenv local python3.8 python3.7 python3.6 python3.5 python2.7
+RUN pyenv local python3.8 python3.7 python3.6
-RUN pyenv exec python2.7 -m pip install --upgrade pip setuptools wheel && \
- pyenv exec python3.5 -m pip install --upgrade pip setuptools wheel && \
- pyenv exec python3.6 -m pip install --upgrade pip setuptools wheel && \
+RUN pyenv exec python3.6 -m pip install --upgrade pip setuptools wheel && \
pyenv exec python3.7 -m pip install --upgrade pip setuptools wheel && \
pyenv exec python3.8 -m pip install --upgrade pip setuptools wheel
@@ -93,20 +93,8 @@ RUN pyenv exec python3.8 -m pip install \
-r requirements/test-ci-default.txt \
-r requirements/docs.txt \
-r requirements/test-integration.txt \
- -r requirements/pkgutils.txt && \
- pyenv exec python3.5 -m pip install \
- -r requirements/dev.txt \
- -r requirements/test.txt \
- -r requirements/test-ci-default.txt \
- -r requirements/docs.txt \
- -r requirements/test-integration.txt \
- -r requirements/pkgutils.txt && \
- pyenv exec python2.7 -m pip install \
- -r requirements/dev.txt \
- -r requirements/test.txt \
- -r requirements/test-ci-default.txt \
- -r requirements/test-integration.txt \
-r requirements/pkgutils.txt
+
COPY --chown=1000:1000 . $HOME/celery
diff --git a/docker/scripts/install-pyenv.sh b/docker/scripts/install-pyenv.sh
index c52a0b807c1..65c06c3343e 100644
--- a/docker/scripts/install-pyenv.sh
+++ b/docker/scripts/install-pyenv.sh
@@ -7,8 +7,6 @@ curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv
git clone https://github.com/s1341/pyenv-alias.git $(pyenv root)/plugins/pyenv-alias
# Python versions to test against
-VERSION_ALIAS="python2.7" pyenv install 2.7.17
-VERSION_ALIAS="python3.5" pyenv install 3.5.8
-VERSION_ALIAS="python3.6" pyenv install 3.6.9
-VERSION_ALIAS="python3.7" pyenv install 3.7.5
-VERSION_ALIAS="python3.8" pyenv install 3.8.0
+VERSION_ALIAS="python3.6" pyenv install 3.6.12
+VERSION_ALIAS="python3.7" pyenv install 3.7.9
+VERSION_ALIAS="python3.8" pyenv install 3.8.7
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 9712c15a2e3..8d28a2924cf 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -1,5 +1,5 @@
pytz>dev
-git+https://github.com/celery/kombu.git
git+https://github.com/celery/py-amqp.git
+git+https://github.com/celery/kombu.git
git+https://github.com/celery/billiard.git
-vine==1.3.0
\ No newline at end of file
+vine>=5.0.0
\ No newline at end of file
diff --git a/requirements/test.txt b/requirements/test.txt
index 92ed354e4c8..2f08e36f734 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,5 +1,5 @@
case>=1.3.1
-pytest~=6.0
+pytest~=6.2
pytest-celery
pytest-subtests
pytest-timeout~=1.4.2
From af270f074acdd417df722d9b387ea959b5d9b653 Mon Sep 17 00:00:00 2001
From: Anna Borzenko
Date: Thu, 24 Dec 2020 17:20:52 +0200
Subject: [PATCH 121/415] Pass DAEMON_OPTS to stopwait in generic celeryd
The stop_workers function in this template init file is missing the $DAEMON_OPTS parameters.
---
extra/generic-init.d/celeryd | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/extra/generic-init.d/celeryd b/extra/generic-init.d/celeryd
index 56d92beac2c..b928eebeb70 100755
--- a/extra/generic-init.d/celeryd
+++ b/extra/generic-init.d/celeryd
@@ -269,7 +269,7 @@ dryrun () {
stop_workers () {
- _chuid stopwait $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+ _chuid stopwait $CELERYD_NODES $DAEMON_OPTS --pidfile="$CELERYD_PID_FILE"
}
From eac0c12a502e742082155561eae50db1b0fad967 Mon Sep 17 00:00:00 2001
From: Myeongseok Seo
Date: Tue, 12 Jan 2021 04:13:02 +0900
Subject: [PATCH 122/415] Update celerybeat (#6550)
* Update celerybeat
Simple change by celery 5.x command format
* Update celerybeat
My previous commit not works, so fixed agian
---
extra/generic-init.d/celerybeat | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/extra/generic-init.d/celerybeat b/extra/generic-init.d/celerybeat
index 8f977903e3a..2667d900a7c 100755
--- a/extra/generic-init.d/celerybeat
+++ b/extra/generic-init.d/celerybeat
@@ -110,7 +110,7 @@ DEFAULT_USER="celery"
DEFAULT_PID_FILE="/var/run/celery/beat.pid"
DEFAULT_LOG_FILE="/var/log/celery/beat.log"
DEFAULT_LOG_LEVEL="INFO"
-DEFAULT_CELERYBEAT="$CELERY_BIN beat"
+DEFAULT_CELERYBEAT="$CELERY_BIN"
CELERYBEAT=${CELERYBEAT:-$DEFAULT_CELERYBEAT}
CELERYBEAT_LOG_LEVEL=${CELERYBEAT_LOG_LEVEL:-${CELERYBEAT_LOGLEVEL:-$DEFAULT_LOG_LEVEL}}
@@ -141,8 +141,6 @@ fi
export CELERY_LOADER
-CELERYBEAT_OPTS="$CELERYBEAT_OPTS -f $CELERYBEAT_LOG_FILE -l $CELERYBEAT_LOG_LEVEL"
-
if [ -n "$2" ]; then
CELERYBEAT_OPTS="$CELERYBEAT_OPTS $2"
fi
@@ -254,8 +252,11 @@ _chuid () {
start_beat () {
echo "Starting ${SCRIPT_NAME}..."
- _chuid $CELERY_APP_ARG $CELERYBEAT_OPTS $DAEMON_OPTS --detach \
- --pidfile="$CELERYBEAT_PID_FILE"
+ _chuid $CELERY_APP_ARG $DAEMON_OPTS beat --detach \
+ --pidfile="$CELERYBEAT_PID_FILE" \
+ --logfile="$CELERYBEAT_LOG_FILE" \
+ --loglevel="$CELERYBEAT_LOG_LEVEL" \
+ $CELERYBEAT_OPTS
}
From d366904284e7d1bc56a2b1a78c01df58748ec5bf Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Tue, 12 Jan 2021 12:47:45 +0600
Subject: [PATCH 123/415] added python 3.9 setup in docker image (#6590)
* added python 3.9 setup in docker image
* fix error
* fix missing stuff
---
CONTRIBUTING.rst | 14 +++++++-------
docker/Dockerfile | 14 +++++++++++---
docker/scripts/install-pyenv.sh | 1 +
3 files changed, 19 insertions(+), 10 deletions(-)
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 12a2aec700d..32000696b49 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -493,19 +493,19 @@ Some useful commands to run:
**Note:** This command will run tests for every environment defined in :file:`tox.ini`.
It takes a while.
-* ``pyenv exec python{3.6,3.7,3.8} -m pytest t/unit``
+* ``pyenv exec python{3.6,3.7,3.8,3.9} -m pytest t/unit``
To run unit tests using pytest.
- **Note:** ``{3.6,3.7,3.8}`` means you can use any of those options.
- e.g. ``pyenv exec python3.6 -m pytest t/unit``
+ **Note:** ``{3.6,3.7,3.8,3.9}`` means you can use any of those options.
+ e.g. ``pyenv exec python3.7 -m pytest t/unit``
-* ``pyenv exec python{3.6,3.7,3.8} -m pytest t/integration``
+* ``pyenv exec python{3.6,3.7,3.8,3.9} -m pytest t/integration``
To run integration tests using pytest
- **Note:** ``{3.6,3.7,3.8}`` means you can use any of those options.
- e.g. ``pyenv exec python3.6 -m pytest t/unit``
+ **Note:** ``{3.6,3.7,3.8,3.9}`` means you can use any of those options.
+ e.g. ``pyenv exec python3.7 -m pytest t/unit``
By default, docker-compose will mount the Celery and test folders in the Docker
container, allowing code changes and testing to be immediately visible inside
@@ -515,7 +515,7 @@ use are also defined in the :file:`docker/docker-compose.yml` file.
By running ``docker-compose build celery`` an image will be created with the
name ``celery/celery:dev``. This docker image has every dependency needed
for development installed. ``pyenv`` is used to install multiple python
-versions, the docker image offers python 3.6, 3.7 and 3.8.
+versions, the docker image offers python 3.6, 3.7, 3.8 and 3.9.
The default python version is set to 3.8.
The :file:`docker-compose.yml` file defines the necessary environment variables
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 469022f0446..7f91b01cc59 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -5,7 +5,7 @@ ENV PYTHONIOENCODING UTF-8
ARG DEBIAN_FRONTEND=noninteractive
-# Pypy is installed from a package manager because it takes so long to build.
+# Pypy3 is installed from a package manager because it takes so long to build.
RUN apt-get update && apt-get install -y build-essential \
libcurl4-openssl-dev \
libffi-dev \
@@ -66,11 +66,12 @@ COPY --chown=1000:1000 docker/entrypoint /entrypoint
RUN chmod gu+x /entrypoint
# Define the local pyenvs
-RUN pyenv local python3.8 python3.7 python3.6
+RUN pyenv local python3.8 python3.7 python3.6 python3.9
RUN pyenv exec python3.6 -m pip install --upgrade pip setuptools wheel && \
pyenv exec python3.7 -m pip install --upgrade pip setuptools wheel && \
- pyenv exec python3.8 -m pip install --upgrade pip setuptools wheel
+ pyenv exec python3.8 -m pip install --upgrade pip setuptools wheel && \
+ pyenv exec python3.9 -m pip install --upgrade pip setuptools wheel
# Setup one celery environment for basic development use
RUN pyenv exec python3.8 -m pip install \
@@ -93,6 +94,13 @@ RUN pyenv exec python3.8 -m pip install \
-r requirements/test-ci-default.txt \
-r requirements/docs.txt \
-r requirements/test-integration.txt \
+ -r requirements/pkgutils.txt && \
+ pyenv exec python3.9 -m pip install \
+ -r requirements/dev.txt \
+ -r requirements/test.txt \
+ -r requirements/test-ci-default.txt \
+ -r requirements/docs.txt \
+ -r requirements/test-integration.txt \
-r requirements/pkgutils.txt
diff --git a/docker/scripts/install-pyenv.sh b/docker/scripts/install-pyenv.sh
index 65c06c3343e..2f3093ced10 100644
--- a/docker/scripts/install-pyenv.sh
+++ b/docker/scripts/install-pyenv.sh
@@ -10,3 +10,4 @@ git clone https://github.com/s1341/pyenv-alias.git $(pyenv root)/plugins/pyenv-a
VERSION_ALIAS="python3.6" pyenv install 3.6.12
VERSION_ALIAS="python3.7" pyenv install 3.7.9
VERSION_ALIAS="python3.8" pyenv install 3.8.7
+VERSION_ALIAS="python3.9" pyenv install 3.9.1
From 3f6486ed589c6a0ae9c31fbacdb24a7b6e22ed19 Mon Sep 17 00:00:00 2001
From: Christian Clauss
Date: Tue, 12 Jan 2021 14:24:20 +0100
Subject: [PATCH 124/415] GitHub Action to lint Python code (#6564)
* GitHub Action to lint Python code
* Update lint_python.yml
* Update lint_python.yml
* we don't use black yet
* Requirements before tox
* isort: Use the default profile
---
.github/workflows/lint_python.yml | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
create mode 100644 .github/workflows/lint_python.yml
diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml
new file mode 100644
index 00000000000..5dd37639e08
--- /dev/null
+++ b/.github/workflows/lint_python.yml
@@ -0,0 +1,19 @@
+name: lint_python
+on: [pull_request, push]
+jobs:
+ lint_python:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ - run: pip install --upgrade pip wheel
+ - run: pip install bandit codespell flake8 isort pytest pyupgrade tox
+ - run: bandit -r . || true
+ - run: codespell --ignore-words-list="brane,gool,ist,sherif,wil" --quiet-level=2 --skip="*.key" || true
+ - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+ - run: isort . || true
+ - run: pip install -r requirements.txt || true
+ - run: tox || true
+ - run: pytest . || true
+ - run: pytest --doctest-modules . || true
+ - run: shopt -s globstar && pyupgrade --py36-plus **/*.py || true
From 2c9d7ef2387a6a5edd83d4770704ae2b4b4f0c91 Mon Sep 17 00:00:00 2001
From: Christian Clauss
Date: Tue, 12 Jan 2021 14:38:44 +0100
Subject: [PATCH 125/415] GitHub Action: strategy: fail-fast: false
Let's see if any of the tests pass...
---
.github/workflows/python-package.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 190cf18ad54..e7838fceaa3 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -14,6 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
+ fail-fast: false
matrix:
python-version: ['3.6', '3.7', '3.8', '3.9', 'pypy3']
From f9b0231d774eb3965b77515227fa34a1b7f4934f Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 12 Jan 2021 16:00:57 +0200
Subject: [PATCH 126/415] Install libmemcached-dev in CI
libmemcached's header files must be found to build pylibmc.
---
.github/workflows/python-package.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index e7838fceaa3..a52d663b107 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Install apt packages
run: |
- sudo apt-get install -f libcurl4-openssl-dev libssl-dev gnutls-dev httping expect
+ sudo apt-get install -f libcurl4-openssl-dev libssl-dev gnutls-dev httping expect libmemcached-dev
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
From 8beb6fb9be730783c54b4e4c545168b0f3ac91ef Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Tue, 22 Dec 2020 22:44:49 +0100
Subject: [PATCH 127/415] Added docstrings for app.control.inspect API
---
celery/app/control.py | 276 ++++++++++++++++++++++++++++++++++++-
docs/userguide/workers.rst | 201 +--------------------------
2 files changed, 278 insertions(+), 199 deletions(-)
diff --git a/celery/app/control.py b/celery/app/control.py
index 3e5fc65b17c..a35f5cec246 100644
--- a/celery/app/control.py
+++ b/celery/app/control.py
@@ -2,6 +2,14 @@
Client for worker remote control commands.
Server implementation is in :mod:`celery.worker.control`.
+There are two types of remote control commands:
+
+* Inspect commands: Does not have side effects, will usually just return some value
+ found in the worker, like the list of currently registered tasks, the list of active tasks, etc.
+ Commands are accessible via :class:`Inspect` class.
+
+* Control commands: Performs side effects, like adding a new queue to consume from.
+ Commands are accessible via :class:`Control` class.
"""
import warnings
@@ -61,7 +69,11 @@ def _after_fork_cleanup_control(control):
class Inspect:
- """API for app.control.inspect."""
+ """API for inspecting workers.
+
+ This class provides proxy for accessing Inspect API of workers. The API is
+ defined in :py:mod:`celery.worker.control`
+ """
app = None
@@ -103,42 +115,254 @@ def _request(self, command, **kwargs):
))
def report(self):
+ """Return human readable report for each worker.
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: {'ok': REPORT_STRING}}``.
+ """
return self._request('report')
def clock(self):
+ """Get the Clock value on workers.
+
+ >>> app.control.inspect().clock()
+ {'celery@node1': {'clock': 12}}
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: CLOCK_VALUE}``.
+ """
return self._request('clock')
def active(self, safe=None):
- # safe is ignored since 4.0
- # as no objects will need serialization now that we
- # have argsrepr/kwargsrepr.
+ """Return list of tasks currently executed by workers.
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: [TASK_INFO,...]}``.
+
+ See Also:
+ For ``TASK_INFO`` details see :func:`query_task` return value.
+
+ Note:
+ ``safe`` is ignored since 4.0 as no objects will need
+ serialization now that we have argsrepr/kwargsrepr.
+ """
return self._request('active')
def scheduled(self, safe=None):
+ """Return list of scheduled tasks with details.
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: [TASK_SCHEDULED_INFO,...]}``.
+
+ Here is the list of ``TASK_SCHEDULED_INFO`` fields:
+
+ * ``eta`` - scheduled time for task execution as string in ISO 8601 format
+ * ``priority`` - priority of the task
+ * ``request`` - field containing ``TASK_INFO`` value.
+
+ See Also:
+ For more details about ``TASK_INFO`` see :func:`query_task` return value.
+ """
return self._request('scheduled')
def reserved(self, safe=None):
+ """Return list of currently reserved tasks, not including scheduled/active.
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: [TASK_INFO,...]}``.
+
+ See Also:
+ For ``TASK_INFO`` details see :func:`query_task` return value.
+ """
return self._request('reserved')
def stats(self):
+ """Return statistics of worker.
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: STAT_INFO}``.
+
+ Here is the list of ``STAT_INFO`` fields:
+
+ * ``broker`` - Section for broker information.
+ * ``connect_timeout`` - Timeout in seconds (int/float) for establishing a new connection.
+ * ``heartbeat`` - Current heartbeat value (set by client).
+ * ``hostname`` - Node name of the remote broker.
+ * ``insist`` - No longer used.
+ * ``login_method`` - Login method used to connect to the broker.
+ * ``port`` - Port of the remote broker.
+ * ``ssl`` - SSL enabled/disabled.
+ * ``transport`` - Name of transport used (e.g., amqp or redis)
+ * ``transport_options`` - Options passed to transport.
+ * ``uri_prefix`` - Some transports expects the host name to be a URL.
+ E.g. ``redis+socket:///tmp/redis.sock``.
+ In this example the URI-prefix will be redis.
+ * ``userid`` - User id used to connect to the broker with.
+ * ``virtual_host`` - Virtual host used.
+ * ``clock`` - Value of the workers logical clock. This is a positive integer
+ and should be increasing every time you receive statistics.
+ * ``uptime`` - Numbers of seconds since the worker controller was started
+ * ``pid`` - Process id of the worker instance (Main process).
+ * ``pool`` - Pool-specific section.
+ * ``max-concurrency`` - Max number of processes/threads/green threads.
+ * ``max-tasks-per-child`` - Max number of tasks a thread may execute before being recycled.
+ * ``processes`` - List of PIDs (or thread-id’s).
+ * ``put-guarded-by-semaphore`` - Internal
+ * ``timeouts`` - Default values for time limits.
+ * ``writes`` - Specific to the prefork pool, this shows the distribution
+ of writes to each process in the pool when using async I/O.
+ * ``prefetch_count`` - Current prefetch count value for the task consumer.
+ * ``rusage`` - System usage statistics. The fields available may be different on your platform.
+ From :manpage:`getrusage(2)`:
+
+ * ``stime`` - Time spent in operating system code on behalf of this process.
+ * ``utime`` - Time spent executing user instructions.
+ * ``maxrss`` - The maximum resident size used by this process (in kilobytes).
+ * ``idrss`` - Amount of non-shared memory used for data (in kilobytes times
+ ticks of execution)
+ * ``isrss`` - Amount of non-shared memory used for stack space
+ (in kilobytes times ticks of execution)
+ * ``ixrss`` - Amount of memory shared with other processes
+ (in kilobytes times ticks of execution).
+ * ``inblock`` - Number of times the file system had to read from the disk
+ on behalf of this process.
+ * ``oublock`` - Number of times the file system has to write to disk
+ on behalf of this process.
+ * ``majflt`` - Number of page faults that were serviced by doing I/O.
+ * ``minflt`` - Number of page faults that were serviced without doing I/O.
+ * ``msgrcv`` - Number of IPC messages received.
+ * ``msgsnd`` - Number of IPC messages sent.
+ * ``nvcsw`` - Number of times this process voluntarily invoked a context switch.
+ * ``nivcsw`` - Number of times an involuntary context switch took place.
+ * ``nsignals`` - Number of signals received.
+ * ``nswap`` - The number of times this process was swapped entirely
+ out of memory.
+ * ``total`` - Map of task names and the total number of tasks with that type
+ the worker has accepted since start-up.
+ """
return self._request('stats')
def revoked(self):
+ """Return list of revoked tasks.
+
+ >>> app.control.inspect().revoked()
+ {'celery@node1': ['16f527de-1c72-47a6-b477-c472b92fef7a']}
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: [TASK_ID, ...]}``.
+ """
return self._request('revoked')
def registered(self, *taskinfoitems):
+ """Return all registered tasks per worker.
+
+ >>> app.control.inspect().registered()
+ {'celery@node1': ['task1', 'task1']}
+ >>> app.control.inspect().registered('serializer', 'max_retries')
+ {'celery@node1': ['task_foo [serializer=json max_retries=3]', 'tasb_bar [serializer=json max_retries=3]']}
+
+ Arguments:
+ taskinfoitems (Sequence[str]): List of :class:`~celery.app.task.Task`
+ attributes to include.
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: [TASK1_INFO, ...]}``.
+ """
return self._request('registered', taskinfoitems=taskinfoitems)
registered_tasks = registered
def ping(self, destination=None):
+ """Ping all (or specific) workers.
+
+ >>> app.control.inspect().ping()
+ {'celery@node1': {'ok': 'pong'}, 'celery@node2': {'ok': 'pong'}}
+ >>> app.control.inspect().ping(destination=['celery@node1'])
+ {'celery@node1': {'ok': 'pong'}}
+
+ Arguments:
+ destination (List): If set, a list of the hosts to send the
+ command to, when empty broadcast to all workers.
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: {'ok': 'pong'}}``.
+
+ See Also:
+ :meth:`broadcast` for supported keyword arguments.
+ """
if destination:
self.destination = destination
return self._request('ping')
def active_queues(self):
+ """Return information about queues from which worker consumes tasks.
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: [QUEUE_INFO, QUEUE_INFO,...]}``.
+
+ Here is the list of ``QUEUE_INFO`` fields:
+
+ * ``name``
+ * ``exchange``
+ * ``name``
+ * ``type``
+ * ``arguments``
+ * ``durable``
+ * ``passive``
+ * ``auto_delete``
+ * ``delivery_mode``
+ * ``no_declare``
+ * ``routing_key``
+ * ``queue_arguments``
+ * ``binding_arguments``
+ * ``consumer_arguments``
+ * ``durable``
+ * ``exclusive``
+ * ``auto_delete``
+ * ``no_ack``
+ * ``alias``
+ * ``bindings``
+ * ``no_declare``
+ * ``expires``
+ * ``message_ttl``
+ * ``max_length``
+ * ``max_length_bytes``
+ * ``max_priority``
+
+ See Also:
+ See the RabbitMQ/AMQP documentation for more details about
+ ``queue_info`` fields.
+ Note:
+ The ``queue_info`` fields are RabbitMQ/AMQP oriented.
+ Not all fields applies for other transports.
+ """
return self._request('active_queues')
def query_task(self, *ids):
+ """Return detail of tasks currently executed by workers.
+
+ Arguments:
+ *ids (str): IDs of tasks to be queried.
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: {TASK_ID: [STATE, TASK_INFO]}}``.
+
+ Here is the list of ``TASK_INFO`` fields:
+ * ``id`` - ID of the task
+ * ``name`` - Name of the task
+ * ``args`` - Positinal arguments passed to the task
+ * ``kwargs`` - Keyword arguments passed to the task
+ * ``type`` - Type of the task
+ * ``hostname`` - Hostname of the worker processing the task
+ * ``time_start`` - Time of processing start
+ * ``acknowledged`` - True when task was acknowledged to broker
+ * ``delivery_info`` - Dictionary containing delivery information
+ * ``exchange`` - Name of exchange where task was published
+ * ``routing_key`` - Routing key used when task was published
+ * ``priority`` - Priority used when task was published
+ * ``redelivered`` - True if the task was redelivered
+ * ``worker_pid`` - PID of worker processin the task
+
+ """
# signature used be unary: query_task(ids=[id1, id2])
# we need this to preserve backward compatibility.
if len(ids) == 1 and isinstance(ids[0], (list, tuple)):
@@ -146,18 +370,54 @@ def query_task(self, *ids):
return self._request('query_task', ids=ids)
def conf(self, with_defaults=False):
+ """Return configuration of each worker.
+
+ Arguments:
+ with_defaults (bool): if set to True, method returns also
+ configuration options with default values.
+
+ Returns:
+ Dict: Dictionary ``{HOSTNAME: WORKER_CONFIGURATION}``.
+
+ See Also:
+ ``WORKER_CONFIGURATION`` is a dictionary containing current configuration options.
+ See :ref:`configuration` for possible values.
+ """
return self._request('conf', with_defaults=with_defaults)
def hello(self, from_node, revoked=None):
return self._request('hello', from_node=from_node, revoked=revoked)
def memsample(self):
+ """Return sample current RSS memory usage.
+
+ Note:
+ Requires the psutils library.
+ """
return self._request('memsample')
def memdump(self, samples=10):
+ """Dump statistics of previous memsample requests.
+
+ Note:
+ Requires the psutils library.
+ """
return self._request('memdump', samples=samples)
def objgraph(self, type='Request', n=200, max_depth=10):
+ """Create graph of uncollected objects (memory-leak debugging).
+
+ Arguments:
+ n (int): Max number of objects to graph.
+ max_depth (int): Traverse at most n levels deep.
+ type (str): Name of object to graph. Default is ``"Request"``.
+
+ Returns:
+ Dict: Dictionary ``{'filename': FILENAME}``
+
+ Note:
+ Requires the objgraph library.
+ """
return self._request('objgraph', num=n, max_depth=max_depth, type=type)
@@ -185,6 +445,7 @@ def _after_fork(self):
@cached_property
def inspect(self):
+ """Create new :class:`Inspect` instance."""
return self.app.subclass_with_self(Inspect, reverse='control.inspect')
def purge(self, connection=None):
@@ -252,8 +513,13 @@ def terminate(self, task_id,
def ping(self, destination=None, timeout=1.0, **kwargs):
"""Ping all (or specific) workers.
+ >>> app.control.ping()
+ [{'celery@node1': {'ok': 'pong'}}, {'celery@node2': {'ok': 'pong'}}]
+ >>> app.control.ping(destination=['celery@node2'])
+ [{'celery@node2': {'ok': 'pong'}}]
+
Returns:
- List[Dict]: List of ``{'hostname': reply}`` dictionaries.
+ List[Dict]: List of ``{HOSTNAME: {'ok': 'pong'}}`` dictionaries.
See Also:
:meth:`broadcast` for supported keyword arguments.
diff --git a/docs/userguide/workers.rst b/docs/userguide/workers.rst
index aec8c9e5414..d87b14f6e18 100644
--- a/docs/userguide/workers.rst
+++ b/docs/userguide/workers.rst
@@ -732,7 +732,7 @@ to specify the workers that should reply to the request:
This can also be done programmatically by using the
-:meth:`@control.inspect.active_queues` method:
+:meth:`~celery.app.control.Inspect.active_queues` method:
.. code-block:: pycon
@@ -771,7 +771,7 @@ Dump of registered tasks
------------------------
You can get a list of tasks registered in the worker using the
-:meth:`~@control.inspect.registered`:
+:meth:`~celery.app.control.Inspect.registered`:
.. code-block:: pycon
@@ -785,7 +785,7 @@ Dump of currently executing tasks
---------------------------------
You can get a list of active tasks using
-:meth:`~@control.inspect.active`:
+:meth:`~celery.app.control.Inspect.active`:
.. code-block:: pycon
@@ -802,7 +802,7 @@ Dump of scheduled (ETA) tasks
-----------------------------
You can get a list of tasks waiting to be scheduled by using
-:meth:`~@control.inspect.scheduled`:
+:meth:`~celery.app.control.Inspect.scheduled`:
.. code-block:: pycon
@@ -834,7 +834,7 @@ Reserved tasks are tasks that have been received, but are still waiting to be
executed.
You can get a list of these using
-:meth:`~@control.inspect.reserved`:
+:meth:`~celery.app.control.Inspect.reserved`:
.. code-block:: pycon
@@ -852,201 +852,14 @@ Statistics
----------
The remote control command ``inspect stats`` (or
-:meth:`~@control.inspect.stats`) will give you a long list of useful (or not
+:meth:`~celery.app.control.Inspect.stats`) will give you a long list of useful (or not
so useful) statistics about the worker:
.. code-block:: console
$ celery -A proj inspect stats
-The output will include the following fields:
-
-- ``broker``
-
- Section for broker information.
-
- * ``connect_timeout``
-
- Timeout in seconds (int/float) for establishing a new connection.
-
- * ``heartbeat``
-
- Current heartbeat value (set by client).
-
- * ``hostname``
-
- Node name of the remote broker.
-
- * ``insist``
-
- No longer used.
-
- * ``login_method``
-
- Login method used to connect to the broker.
-
- * ``port``
-
- Port of the remote broker.
-
- * ``ssl``
-
- SSL enabled/disabled.
-
- * ``transport``
-
- Name of transport used (e.g., ``amqp`` or ``redis``)
-
- * ``transport_options``
-
- Options passed to transport.
-
- * ``uri_prefix``
-
- Some transports expects the host name to be a URL.
-
- .. code-block:: text
-
- redis+socket:///tmp/redis.sock
-
- In this example the URI-prefix will be ``redis``.
-
- * ``userid``
-
- User id used to connect to the broker with.
-
- * ``virtual_host``
-
- Virtual host used.
-
-- ``clock``
-
- Value of the workers logical clock. This is a positive integer and should
- be increasing every time you receive statistics.
-
-- ``uptime``
-
- Numbers of seconds since the worker controller was started
-
-- ``pid``
-
- Process id of the worker instance (Main process).
-
-- ``pool``
-
- Pool-specific section.
-
- * ``max-concurrency``
-
- Max number of processes/threads/green threads.
-
- * ``max-tasks-per-child``
-
- Max number of tasks a thread may execute before being recycled.
-
- * ``processes``
-
- List of PIDs (or thread-id's).
-
- * ``put-guarded-by-semaphore``
-
- Internal
-
- * ``timeouts``
-
- Default values for time limits.
-
- * ``writes``
-
- Specific to the prefork pool, this shows the distribution of writes
- to each process in the pool when using async I/O.
-
-- ``prefetch_count``
-
- Current prefetch count value for the task consumer.
-
-- ``rusage``
-
- System usage statistics. The fields available may be different
- on your platform.
-
- From :manpage:`getrusage(2)`:
-
- * ``stime``
-
- Time spent in operating system code on behalf of this process.
-
- * ``utime``
-
- Time spent executing user instructions.
-
- * ``maxrss``
-
- The maximum resident size used by this process (in kilobytes).
-
- * ``idrss``
-
- Amount of non-shared memory used for data (in kilobytes times ticks of
- execution)
-
- * ``isrss``
-
- Amount of non-shared memory used for stack space (in kilobytes times
- ticks of execution)
-
- * ``ixrss``
-
- Amount of memory shared with other processes (in kilobytes times
- ticks of execution).
-
- * ``inblock``
-
- Number of times the file system had to read from the disk on behalf of
- this process.
-
- * ``oublock``
-
- Number of times the file system has to write to disk on behalf of
- this process.
-
- * ``majflt``
-
- Number of page faults that were serviced by doing I/O.
-
- * ``minflt``
-
- Number of page faults that were serviced without doing I/O.
-
- * ``msgrcv``
-
- Number of IPC messages received.
-
- * ``msgsnd``
-
- Number of IPC messages sent.
-
- * ``nvcsw``
-
- Number of times this process voluntarily invoked a context switch.
-
- * ``nivcsw``
-
- Number of times an involuntary context switch took place.
-
- * ``nsignals``
-
- Number of signals received.
-
- * ``nswap``
-
- The number of times this process was swapped entirely out of memory.
-
-
-- ``total``
-
- Map of task names and the total number of tasks with that type
- the worker has accepted since start-up.
-
+For the output details, consult the reference documentation of :meth:`~celery.app.control.Inspect.stats`.
Additional Commands
===================
From 7dc76ff3bd93ffca9abcc8130b6eea436a6bae49 Mon Sep 17 00:00:00 2001
From: Matt Hoffman
Date: Tue, 12 Jan 2021 12:09:53 -0500
Subject: [PATCH 128/415] Makes regen less greedy (#6589)
* Makes regen less greedy
Might fix #4298. This was originally part of https://github.com/celery/celery/pull/6576.
* adds assertion to ensure regen item is not lost
---
celery/utils/functional.py | 25 ++++++++++++++++++---
t/unit/utils/test_functional.py | 39 ++++++++++++++++++++++++---------
2 files changed, 51 insertions(+), 13 deletions(-)
diff --git a/celery/utils/functional.py b/celery/utils/functional.py
index b28e4a3ba48..68172cc2067 100644
--- a/celery/utils/functional.py
+++ b/celery/utils/functional.py
@@ -3,7 +3,7 @@
import sys
from collections import UserList
from functools import partial
-from itertools import chain, islice
+from itertools import islice
from kombu.utils.functional import (LRUCache, dictfilter, is_list, lazy,
maybe_evaluate, maybe_list, memoize)
@@ -182,6 +182,7 @@ def __init__(self, it):
self.__it = it
self.__index = 0
self.__consumed = []
+ self.__done = False
def __reduce__(self):
return list, (self.data,)
@@ -190,7 +191,13 @@ def __length_hint__(self):
return self.__it.__length_hint__()
def __iter__(self):
- return chain(self.__consumed, self.__it)
+ for x in self.__consumed:
+ yield x
+ if not self.__done:
+ for x in self.__it:
+ self.__consumed.append(x)
+ yield x
+ self.__done = True
def __getitem__(self, index):
if index < 0:
@@ -198,14 +205,26 @@ def __getitem__(self, index):
try:
return self.__consumed[index]
except IndexError:
+ it = iter(self)
try:
for _ in range(self.__index, index + 1):
- self.__consumed.append(next(self.__it))
+ next(it)
except StopIteration:
raise IndexError(index)
else:
return self.__consumed[index]
+ def __bool__(self):
+ if len(self.__consumed):
+ return True
+
+ try:
+ next(iter(self))
+ except StopIteration:
+ return False
+ else:
+ return True
+
@property
def data(self):
try:
diff --git a/t/unit/utils/test_functional.py b/t/unit/utils/test_functional.py
index 503b7476655..0eead299908 100644
--- a/t/unit/utils/test_functional.py
+++ b/t/unit/utils/test_functional.py
@@ -1,11 +1,10 @@
import pytest
-from kombu.utils.functional import lazy
-
from celery.utils.functional import (DummyContext, first, firstmethod,
fun_accepts_kwargs, fun_takes_argument,
head_from_fun, maybe_list, mlazy,
padlist, regen, seq_concat_item,
seq_concat_seq)
+from kombu.utils.functional import lazy
def test_DummyContext():
@@ -94,8 +93,11 @@ def test_list(self):
fun, args = r.__reduce__()
assert fun(*args) == l
- def test_gen(self):
- g = regen(iter(list(range(10))))
+ @pytest.fixture
+ def g(self):
+ return regen(iter(list(range(10))))
+
+ def test_gen(self, g):
assert g[7] == 7
assert g[6] == 6
assert g[5] == 5
@@ -107,17 +109,19 @@ def test_gen(self):
assert g.data, list(range(10))
assert g[8] == 8
assert g[0] == 0
- g = regen(iter(list(range(10))))
+
+ def test_gen__index_2(self, g):
assert g[0] == 0
assert g[1] == 1
assert g.data == list(range(10))
- g = regen(iter([1]))
- assert g[0] == 1
+
+ def test_gen__index_error(self, g):
+ assert g[0] == 0
with pytest.raises(IndexError):
- g[1]
- assert g.data == [1]
+ g[11]
+ assert list(iter(g)) == list(range(10))
- g = regen(iter(list(range(10))))
+ def test_gen__negative_index(self, g):
assert g[-1] == 9
assert g[-2] == 8
assert g[-3] == 7
@@ -128,6 +132,21 @@ def test_gen(self):
assert list(iter(g)) == list(range(10))
+ def test_nonzero__does_not_consume_more_than_first_item(self):
+ def build_generator():
+ yield 1
+ self.consumed_second_item = True
+ yield 2
+
+ self.consumed_second_item = False
+ g = regen(build_generator())
+ assert bool(g)
+ assert g[0] == 1
+ assert not self.consumed_second_item
+
+ def test_nonzero__empty_iter(self):
+ assert not regen(iter([]))
+
class test_head_from_fun:
From 86c3673c0a11190a7acdd49c1f4cb184395bb6dd Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 13 Jan 2021 10:01:02 +0200
Subject: [PATCH 129/415] Instead of yielding each item, yield from the entire
consumed list first.
---
celery/utils/functional.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/celery/utils/functional.py b/celery/utils/functional.py
index 68172cc2067..ab36e3d4c3d 100644
--- a/celery/utils/functional.py
+++ b/celery/utils/functional.py
@@ -191,8 +191,7 @@ def __length_hint__(self):
return self.__it.__length_hint__()
def __iter__(self):
- for x in self.__consumed:
- yield x
+ yield from self.__consumed
if not self.__done:
for x in self.__it:
self.__consumed.append(x)
From 3a61302efda8db58f9259a72844a93a4bc3be5d2 Mon Sep 17 00:00:00 2001
From: Jonathan Stoppani
Date: Thu, 14 Jan 2021 09:48:20 +0100
Subject: [PATCH 130/415] Pytest worker shutdown timeout (#6588)
* Raise an exception if the worker thread does not exit in 10s
* Allow to override the worker shutdown timeout
* Set daemon=True whens starting worker thread
* Remove TODO
---
celery/contrib/testing/worker.py | 13 +++++++++++--
docs/userguide/testing.rst | 5 +++++
2 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/celery/contrib/testing/worker.py b/celery/contrib/testing/worker.py
index 78cc5951fb8..16d2582897d 100644
--- a/celery/contrib/testing/worker.py
+++ b/celery/contrib/testing/worker.py
@@ -59,6 +59,7 @@ def start_worker(
logfile=None, # type: str
perform_ping_check=True, # type: bool
ping_task_timeout=10.0, # type: float
+ shutdown_timeout=10.0, # type: float
**kwargs # type: Any
):
# type: (...) -> Iterable
@@ -75,6 +76,7 @@ def start_worker(
loglevel=loglevel,
logfile=logfile,
perform_ping_check=perform_ping_check,
+ shutdown_timeout=shutdown_timeout,
**kwargs) as worker:
if perform_ping_check:
from .tasks import ping
@@ -93,6 +95,7 @@ def _start_worker_thread(app,
logfile=None,
WorkController=TestWorkController,
perform_ping_check=True,
+ shutdown_timeout=10.0,
**kwargs):
# type: (Celery, int, str, Union[str, int], str, Any, **Any) -> Iterable
"""Start Celery worker in a thread.
@@ -121,7 +124,7 @@ def _start_worker_thread(app,
without_gossip=True,
**kwargs)
- t = threading.Thread(target=worker.start)
+ t = threading.Thread(target=worker.start, daemon=True)
t.start()
worker.ensure_started()
_set_task_join_will_block(False)
@@ -130,7 +133,13 @@ def _start_worker_thread(app,
from celery.worker import state
state.should_terminate = 0
- t.join(10)
+ t.join(shutdown_timeout)
+ if t.is_alive():
+ raise RuntimeError(
+ "Worker thread failed to exit within the allocated timeout. "
+ "Consider raising `shutdown_timeout` if your tasks take longer "
+ "to execute."
+ )
state.should_terminate = None
diff --git a/docs/userguide/testing.rst b/docs/userguide/testing.rst
index 1df28b21978..94389c30739 100644
--- a/docs/userguide/testing.rst
+++ b/docs/userguide/testing.rst
@@ -167,6 +167,11 @@ This fixture starts a Celery worker instance that you can use
for integration tests. The worker will be started in a *separate thread*
and will be shutdown as soon as the test returns.
+By default the fixture will wait up to 10 seconds for the worker to complete
+outstanding tasks and will raise an exception if the time limit is exceeded.
+The timeout can be customized by setting the ``shutdown_timeout`` key in the
+dictionary returned by the :func:`celery_worker_parameters` fixture.
+
Example:
.. code-block:: python
From 7161414b6332c88bd124316b7927ac8bb416f8b3 Mon Sep 17 00:00:00 2001
From: "Asif Saif Uddin (Auvi)"
Date: Fri, 15 Jan 2021 23:00:25 +0600
Subject: [PATCH 131/415] fix isort
---
celery/backends/azureblockblob.py | 3 ++-
t/integration/test_inspect.py | 4 ++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/celery/backends/azureblockblob.py b/celery/backends/azureblockblob.py
index 93ff600a23d..81b15f6dec0 100644
--- a/celery/backends/azureblockblob.py
+++ b/celery/backends/azureblockblob.py
@@ -9,8 +9,9 @@
try:
import azure.storage.blob as azurestorage
+ from azure.core.exceptions import (ResourceExistsError,
+ ResourceNotFoundError)
from azure.storage.blob import BlobServiceClient
- from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
except ImportError:
azurestorage = None
diff --git a/t/integration/test_inspect.py b/t/integration/test_inspect.py
index 49275622aa2..6070de483d2 100644
--- a/t/integration/test_inspect.py
+++ b/t/integration/test_inspect.py
@@ -1,14 +1,14 @@
import os
import re
from datetime import datetime, timedelta
-from unittest.mock import ANY
from time import sleep
+from unittest.mock import ANY
import pytest
from celery.utils.nodenames import anon_nodename
-from .tasks import sleeping, add
+from .tasks import add, sleeping
NODENAME = anon_nodename()
From 17eda8de2de77616d950ddf2fc1ebec5b5c85a7e Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Tue, 19 Jan 2021 16:45:57 +0600
Subject: [PATCH 132/415] fix possible typo (#6606)
---
docs/whatsnew-5.0.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/whatsnew-5.0.rst b/docs/whatsnew-5.0.rst
index 7e38c924a13..d2e2df90e62 100644
--- a/docs/whatsnew-5.0.rst
+++ b/docs/whatsnew-5.0.rst
@@ -301,7 +301,7 @@ Celery 4.4.7 introduced an opt-in feature to make them ordered.
It is now an opt-out behavior.
If you were previously using the Redis result backend, you might need to
-out-out of this behavior.
+opt-out of this behavior.
Please refer to the :ref:`documentation `
for instructions on how to disable this feature.
From 2551162c02074921de4ddef89684e72590e9d396 Mon Sep 17 00:00:00 2001
From: tned73
Date: Tue, 19 Jan 2021 18:32:53 +0100
Subject: [PATCH 133/415] exit celery with non zero exit value if failing
(#6602)
---
celery/bin/control.py | 19 +++++++++++++------
celery/exceptions.py | 10 ++++++++++
2 files changed, 23 insertions(+), 6 deletions(-)
diff --git a/celery/bin/control.py b/celery/bin/control.py
index 3fe8eb76b42..507c5ec8efb 100644
--- a/celery/bin/control.py
+++ b/celery/bin/control.py
@@ -6,6 +6,7 @@
from celery.bin.base import (COMMA_SEPARATED_LIST, CeleryCommand,
CeleryOption, handle_preload_options)
+from celery.exceptions import CeleryCommandException
from celery.platforms import EX_UNAVAILABLE
from celery.utils import text
from celery.worker.control import Panel
@@ -81,8 +82,10 @@ def status(ctx, timeout, destination, json, **kwargs):
callback=callback).ping()
if not replies:
- ctx.obj.echo('No nodes replied within time constraint')
- return EX_UNAVAILABLE
+ raise CeleryCommandException(
+ message='No nodes replied within time constraint',
+ exit_code=EX_UNAVAILABLE
+ )
if json:
ctx.obj.echo(dumps(replies))
@@ -130,8 +133,10 @@ def inspect(ctx, action, timeout, destination, json, **kwargs):
callback=callback)._request(action)
if not replies:
- ctx.obj.echo('No nodes replied within time constraint')
- return EX_UNAVAILABLE
+ raise CeleryCommandException(
+ message='No nodes replied within time constraint',
+ exit_code=EX_UNAVAILABLE
+ )
if json:
ctx.obj.echo(dumps(replies))
@@ -184,8 +189,10 @@ def control(ctx, action, timeout, destination, json):
arguments=arguments)
if not replies:
- ctx.obj.echo('No nodes replied within time constraint')
- return EX_UNAVAILABLE
+ raise CeleryCommandException(
+ message='No nodes replied within time constraint',
+ exit_code=EX_UNAVAILABLE
+ )
if json:
ctx.obj.echo(dumps(replies))
diff --git a/celery/exceptions.py b/celery/exceptions.py
index 768cd4d22d2..5db3a803aef 100644
--- a/celery/exceptions.py
+++ b/celery/exceptions.py
@@ -54,6 +54,7 @@
from billiard.exceptions import (SoftTimeLimitExceeded, Terminated,
TimeLimitExceeded, WorkerLostError)
+from click import ClickException
from kombu.exceptions import OperationalError
__all__ = (
@@ -91,6 +92,8 @@
# Worker shutdown semi-predicates (inherits from SystemExit).
'WorkerShutdown', 'WorkerTerminate',
+
+ 'CeleryCommandException',
)
UNREGISTERED_FMT = """\
@@ -293,3 +296,10 @@ def __init__(self, *args, **kwargs):
def __repr__(self):
return super().__repr__() + " state:" + self.state + " task_id:" + self.task_id
+
+
+class CeleryCommandException(ClickException):
+
+ def __init__(self, message, exit_code):
+ super().__init__(message=message)
+ self.exit_code = exit_code
From d465a84e26de9eea35c7d6f9438813f6787497e7 Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Thu, 21 Jan 2021 00:47:03 +0600
Subject: [PATCH 134/415] Docs clean up (#6607)
* update minimum supported django version to 1.11lts for celery 5.0.x docs
* doc code cleanup
* docs code cleanup
* docs code cleanup
* Update docs/django/first-steps-with-django.rst
Co-authored-by: Omer Katz
Co-authored-by: Omer Katz
---
docs/django/first-steps-with-django.rst | 4 ++--
docs/internals/app-overview.rst | 2 +-
docs/internals/guide.rst | 12 ++++++------
docs/internals/protocol.rst | 2 +-
docs/userguide/application.rst | 6 +++---
docs/userguide/canvas.rst | 2 +-
docs/userguide/configuration.rst | 2 +-
docs/userguide/tasks.rst | 2 +-
8 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/docs/django/first-steps-with-django.rst b/docs/django/first-steps-with-django.rst
index f3a20b18a48..7a0727885e1 100644
--- a/docs/django/first-steps-with-django.rst
+++ b/docs/django/first-steps-with-django.rst
@@ -19,8 +19,8 @@ Using Celery with Django
.. note::
- Celery 4.0 supports Django 1.8 and newer versions. Please use Celery 3.1
- for versions older than Django 1.8.
+ Celery 5.0.x supports Django 1.11 LTS or newer versions. Please use Celery 4.4.x
+ for versions older than Django 1.11.
To use Celery with your Django project you must first define
an instance of the Celery library (called an "app")
diff --git a/docs/internals/app-overview.rst b/docs/internals/app-overview.rst
index a46021e105b..3634a5f8060 100644
--- a/docs/internals/app-overview.rst
+++ b/docs/internals/app-overview.rst
@@ -176,7 +176,7 @@ is missing.
from celery.app import app_or_default
- class SomeClass(object):
+ class SomeClass:
def __init__(self, app=None):
self.app = app_or_default(app)
diff --git a/docs/internals/guide.rst b/docs/internals/guide.rst
index e7d600da275..731cacbaac4 100644
--- a/docs/internals/guide.rst
+++ b/docs/internals/guide.rst
@@ -53,10 +53,10 @@ Naming
pass
# - "action" class (verb)
- class UpdateTwitterStatus(object): # BAD
+ class UpdateTwitterStatus: # BAD
pass
- class update_twitter_status(object): # GOOD
+ class update_twitter_status: # GOOD
pass
.. note::
@@ -71,7 +71,7 @@ Naming
.. code-block:: python
- class Celery(object):
+ class Celery:
def consumer_factory(self): # BAD
...
@@ -89,7 +89,7 @@ as this means that they can be set by either instantiation or inheritance.
.. code-block:: python
- class Producer(object):
+ class Producer:
active = True
serializer = 'json'
@@ -130,7 +130,7 @@ the exception class from the instance directly.
class Empty(Exception):
pass
- class Queue(object):
+ class Queue:
Empty = Empty
def get(self):
@@ -157,7 +157,7 @@ saved us from many a monkey patch).
.. code-block:: python
- class Worker(object):
+ class Worker:
Consumer = Consumer
def __init__(self, connection, consumer_cls=None):
diff --git a/docs/internals/protocol.rst b/docs/internals/protocol.rst
index 196077213c8..ce4794be83d 100644
--- a/docs/internals/protocol.rst
+++ b/docs/internals/protocol.rst
@@ -168,7 +168,7 @@ Changes from version 1
def apply_async(self, args, kwargs, **options):
fun, real_args = self.unpack_args(*args)
- return super(PickleTask, self).apply_async(
+ return super().apply_async(
(fun, real_args, kwargs), shadow=qualname(fun), **options
)
diff --git a/docs/userguide/application.rst b/docs/userguide/application.rst
index 6ec6c7f8f89..4fb6c665e39 100644
--- a/docs/userguide/application.rst
+++ b/docs/userguide/application.rst
@@ -400,7 +400,7 @@ The following example is considered bad practice:
from celery import current_app
- class Scheduler(object):
+ class Scheduler:
def run(self):
app = current_app
@@ -409,7 +409,7 @@ Instead it should take the ``app`` as an argument:
.. code-block:: python
- class Scheduler(object):
+ class Scheduler:
def __init__(self, app):
self.app = app
@@ -421,7 +421,7 @@ so that everything also works in the module-based compatibility API
from celery.app import app_or_default
- class Scheduler(object):
+ class Scheduler:
def __init__(self, app=None):
self.app = app_or_default(app)
diff --git a/docs/userguide/canvas.rst b/docs/userguide/canvas.rst
index 67c42ba583c..55811f2fbe0 100644
--- a/docs/userguide/canvas.rst
+++ b/docs/userguide/canvas.rst
@@ -951,7 +951,7 @@ implemented in other backends (suggestions welcome!).
def after_return(self, *args, **kwargs):
do_something()
- super(MyTask, self).after_return(*args, **kwargs)
+ super().after_return(*args, **kwargs)
.. _canvas-map:
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index 7142cd6ac16..01e8b7784e7 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -317,7 +317,7 @@ instead of a dict to choose the tasks to annotate:
.. code-block:: python
- class MyAnnotate(object):
+ class MyAnnotate:
def annotate(self, task):
if task.name.startswith('tasks.'):
diff --git a/docs/userguide/tasks.rst b/docs/userguide/tasks.rst
index 58e4125cac9..d44e32dc0fb 100644
--- a/docs/userguide/tasks.rst
+++ b/docs/userguide/tasks.rst
@@ -359,7 +359,7 @@ may contain:
def gen_task_name(self, name, module):
if module.endswith('.tasks'):
module = module[:-6]
- return super(MyCelery, self).gen_task_name(name, module)
+ return super().gen_task_name(name, module)
app = MyCelery('main')
From 7a0c9f95c23c4878603f9e99fd749e588b0394df Mon Sep 17 00:00:00 2001
From: Kojo Idrissa
Date: Wed, 20 Jan 2021 13:47:55 -0600
Subject: [PATCH 135/415] fixed typo in help command
---
docs/getting-started/first-steps-with-celery.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/getting-started/first-steps-with-celery.rst b/docs/getting-started/first-steps-with-celery.rst
index aefaa4aa867..13bdc8cc429 100644
--- a/docs/getting-started/first-steps-with-celery.rst
+++ b/docs/getting-started/first-steps-with-celery.rst
@@ -181,7 +181,7 @@ There are also several other commands available, and help is also available:
.. code-block:: console
- $ celery help
+ $ celery --help
.. _`supervisord`: http://supervisord.org
From 43a692524ca0f4792ee5bcc67764a659a90cde35 Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Fri, 22 Jan 2021 22:52:51 +0600
Subject: [PATCH 136/415] [poc] - celery unit tests with pytest & github action
& some minor tweak in test to make them pass (#6587)
* add initial tox-docker blocks
* add initial tox-docker blocks
* modify tox-docker blocks
* use pytest & github actions matrix to run unit tests instead of tox
* manually install test requirements
* manually install test requirements
* change timeout=3.0 to pass test locally
* drop tox-docker
* Delete 14
* modify tox
---
.github/workflows/python-package.yml | 15 +++++++++------
t/unit/backends/test_base.py | 2 +-
t/unit/backends/test_redis.py | 2 +-
tox.ini | 14 +++++++++-----
4 files changed, 20 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index a52d663b107..dcde3494e78 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -12,7 +12,7 @@ on:
jobs:
build:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
@@ -42,15 +42,18 @@ jobs:
${{ matrix.python-version }}-v1-
- name: Install dependencies
run: |
- python -m pip install --upgrade pip tox tox-gh-actions
- python -m pip install flake8 pytest
+ python -m pip install --upgrade pip
+ python -m pip install flake8 pytest case pytest-celery pytest-subtests pytest-timeout
+ python -m pip install moto boto3 msgpack PyYAML
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+
+ - name: Run Unit test with pytest
+ run: |
+ pytest -xv t/unit
+
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- - name: Run Tox
- run: |
- tox -v
diff --git a/t/unit/backends/test_base.py b/t/unit/backends/test_base.py
index 6f54bdf37f1..c805cb10f03 100644
--- a/t/unit/backends/test_base.py
+++ b/t/unit/backends/test_base.py
@@ -796,7 +796,7 @@ def test_chord_part_return_timeout(self):
self.b.expire.assert_not_called()
deps.delete.assert_called_with()
- deps.join_native.assert_called_with(propagate=True, timeout=4.0)
+ deps.join_native.assert_called_with(propagate=True, timeout=3.0)
def test_chord_part_return_propagate_set(self):
with self._chord_part_context(self.b) as (task, deps, _):
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index 445a9bb10e7..bdf5d9180fd 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -1025,7 +1025,7 @@ def test_on_chord_part_return_timeout(self, complex_header_result):
self.app.conf.result_chord_join_timeout -= 1.0
join_func = complex_header_result.return_value.join_native
- join_func.assert_called_once_with(timeout=4.0, propagate=True)
+ join_func.assert_called_once_with(timeout=3.0, propagate=True)
@pytest.mark.parametrize("supports_native_join", (True, False))
def test_on_chord_part_return(
diff --git a/tox.ini b/tox.ini
index 2196d3d8d47..f62ea3cdff1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,6 +8,7 @@ envlist =
configcheck
bandit
+
[gh-actions]
python =
3.6: 3.6
@@ -17,6 +18,11 @@ python =
pypy3: pypy3
[testenv]
+sitepackages = False
+recreate = False
+passenv =
+ AZUREBLOCKBLOB_URL
+
deps=
-r{toxinidir}/requirements/default.txt
-r{toxinidir}/requirements/test.txt
@@ -32,8 +38,7 @@ deps=
linkcheck,apicheck,configcheck: -r{toxinidir}/requirements/docs.txt
flake8: -r{toxinidir}/requirements/pkgutils.txt
bandit: bandit
-sitepackages = False
-recreate = False
+
commands =
unit: pytest -xv --cov=celery --cov-report=xml --cov-report term {posargs}
integration: pytest -xsv t/integration {posargs}
@@ -64,9 +69,7 @@ setenv =
azureblockblob: TEST_BROKER=redis://
azureblockblob: TEST_BACKEND=azureblockblob://DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;
-passenv =
- TRAVIS
- AZUREBLOCKBLOB_URL
+
basepython =
3.6: python3.6
3.7: python3.7
@@ -76,6 +79,7 @@ basepython =
flake8,apicheck,linkcheck,configcheck,bandit: python3.9
usedevelop = True
+
[testenv:apicheck]
setenv =
PYTHONHASHSEED = 100
From 29eda054555fa95c83210e5e6bc3e839c80bcd3b Mon Sep 17 00:00:00 2001
From: Matt Hoffman
Date: Fri, 22 Jan 2021 15:58:18 -0500
Subject: [PATCH 137/415] fixes github action unit tests using PYTHONPATH
Before the tests were importing from the latest release instead of
loading from the files in this repository.
---
.github/workflows/python-package.yml | 2 +-
t/unit/backends/test_base.py | 2 +-
t/unit/backends/test_redis.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index dcde3494e78..414522b8dc9 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -49,7 +49,7 @@ jobs:
- name: Run Unit test with pytest
run: |
- pytest -xv t/unit
+ PYTHONPATH=. pytest -xv t/unit
- name: Lint with flake8
run: |
diff --git a/t/unit/backends/test_base.py b/t/unit/backends/test_base.py
index c805cb10f03..6f54bdf37f1 100644
--- a/t/unit/backends/test_base.py
+++ b/t/unit/backends/test_base.py
@@ -796,7 +796,7 @@ def test_chord_part_return_timeout(self):
self.b.expire.assert_not_called()
deps.delete.assert_called_with()
- deps.join_native.assert_called_with(propagate=True, timeout=3.0)
+ deps.join_native.assert_called_with(propagate=True, timeout=4.0)
def test_chord_part_return_propagate_set(self):
with self._chord_part_context(self.b) as (task, deps, _):
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index bdf5d9180fd..445a9bb10e7 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -1025,7 +1025,7 @@ def test_on_chord_part_return_timeout(self, complex_header_result):
self.app.conf.result_chord_join_timeout -= 1.0
join_func = complex_header_result.return_value.join_native
- join_func.assert_called_once_with(timeout=3.0, propagate=True)
+ join_func.assert_called_once_with(timeout=4.0, propagate=True)
@pytest.mark.parametrize("supports_native_join", (True, False))
def test_on_chord_part_return(
From c7f2f141627de69645d1885b000b12def97152ec Mon Sep 17 00:00:00 2001
From: kosarchuksn
Date: Tue, 1 Sep 2020 19:37:29 +0300
Subject: [PATCH 138/415] Update task retry docs
---
docs/userguide/tasks.rst | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/docs/userguide/tasks.rst b/docs/userguide/tasks.rst
index d44e32dc0fb..935f15f92c2 100644
--- a/docs/userguide/tasks.rst
+++ b/docs/userguide/tasks.rst
@@ -805,13 +805,13 @@ via options documented below.
.. versionadded:: 4.4
-You can also set `autoretry_for`, `retry_kwargs`, `retry_backoff`, `retry_backoff_max` and `retry_jitter` options in class-based tasks:
+You can also set `autoretry_for`, `max_retries`, `retry_backoff`, `retry_backoff_max` and `retry_jitter` options in class-based tasks:
.. code-block:: python
class BaseTaskWithRetry(Task):
autoretry_for = (TypeError,)
- retry_kwargs = {'max_retries': 5}
+ max_retries = 5
retry_backoff = True
retry_backoff_max = 700
retry_jitter = False
@@ -822,12 +822,10 @@ You can also set `autoretry_for`, `retry_kwargs`, `retry_backoff`, `retry_backof
during the execution of the task, the task will automatically be retried.
By default, no exceptions will be autoretried.
-.. attribute:: Task.retry_kwargs
+.. attribute:: Task.max_retries
- A dictionary. Use this to customize how autoretries are executed.
- Note that if you use the exponential backoff options below, the `countdown`
- task option will be determined by Celery's autoretry system, and any
- `countdown` included in this dictionary will be ignored.
+ A number. Maximum number of retries before giving up. A value of ``None``
+ means task will retry forever.
.. attribute:: Task.retry_backoff
From 023afc1aabe899b189a45499aa469afa39222736 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pavol=20Plasko=C5=88?=
Date: Wed, 16 Dec 2020 13:49:13 +0100
Subject: [PATCH 139/415] Fix a typo in a docstring.
---
celery/exceptions.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/celery/exceptions.py b/celery/exceptions.py
index 5db3a803aef..66b3ca2a341 100644
--- a/celery/exceptions.py
+++ b/celery/exceptions.py
@@ -288,7 +288,7 @@ def __repr__(self):
class BackendStoreError(BackendError):
- """An issue writing from the backend."""
+ """An issue writing to the backend."""
def __init__(self, *args, **kwargs):
self.state = kwargs.get('state', "")
From dd607c623eddd30633d10579be454d48bcbea9bd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pavol=20Plasko=C5=88?=
Date: Sat, 6 Feb 2021 11:39:06 +0100
Subject: [PATCH 140/415] Raise BackendStoreError when set value is too large
for Redis.
See #6533 for details.
---
celery/backends/base.py | 6 +++++-
celery/backends/redis.py | 9 ++++++++-
t/unit/backends/test_redis.py | 6 +++++-
3 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/celery/backends/base.py b/celery/backends/base.py
index 22fe0c79cb9..b18f40887e2 100644
--- a/celery/backends/base.py
+++ b/celery/backends/base.py
@@ -853,7 +853,11 @@ def _store_result(self, task_id, result, state,
if current_meta['status'] == states.SUCCESS:
return result
- self._set_with_state(self.get_key_for_task(task_id), self.encode(meta), state)
+ try:
+ self._set_with_state(self.get_key_for_task(task_id), self.encode(meta), state)
+ except BackendStoreError as ex:
+ raise BackendStoreError(str(ex), state=state, task_id=task_id) from ex
+
return result
def _save_group(self, group_id, result):
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index e767de05c58..a0d392d9527 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -12,7 +12,7 @@
from celery import states
from celery._state import task_join_will_block
from celery.canvas import maybe_signature
-from celery.exceptions import ChordError, ImproperlyConfigured
+from celery.exceptions import BackendStoreError, ChordError, ImproperlyConfigured
from celery.result import GroupResult, allow_join_result
from celery.utils.functional import dictfilter
from celery.utils.log import get_logger
@@ -192,6 +192,10 @@ class RedisBackend(BaseKeyValueStoreBackend, AsyncBackendMixin):
supports_autoexpire = True
supports_native_join = True
+ #: Maximal length of string value in Redis.
+ #: 512 MB - https://redis.io/topics/data-types
+ _MAX_STR_VALUE_SIZE = 536870912
+
def __init__(self, host=None, port=None, db=None, password=None,
max_connections=None, url=None,
connection_pool=None, **kwargs):
@@ -364,6 +368,9 @@ def on_connection_error(self, max_retries, exc, intervals, retries):
return tts
def set(self, key, value, **retry_policy):
+ if len(value) > self._MAX_STR_VALUE_SIZE:
+ raise BackendStoreError('value too large for Redis backend')
+
return self.ensure(self._set, (key, value), **retry_policy)
def _set(self, key, value):
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index 445a9bb10e7..23580fa3dfb 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -12,7 +12,7 @@
from celery import signature, states, uuid
from celery.canvas import Signature
-from celery.exceptions import ChordError, ImproperlyConfigured
+from celery.exceptions import BackendStoreError, ChordError, ImproperlyConfigured
from celery.utils.collections import AttributeDict
@@ -675,6 +675,10 @@ def test_set_expires(self):
key, 512,
)
+ def test_set_raises_error_on_large_value(self):
+ with pytest.raises(BackendStoreError):
+ self.b.set('key', 'x' * (self.b._MAX_STR_VALUE_SIZE + 1))
+
class test_RedisBackend_chords_simple(basetest_RedisBackend):
@pytest.fixture(scope="class", autouse=True)
From 4d71dd8ac1eb9db3e9299a366c11d0e125e6631a Mon Sep 17 00:00:00 2001
From: Anatoliy
Date: Sun, 7 Feb 2021 00:00:33 +0300
Subject: [PATCH 141/415] Update extra/supervisord/celeryd.conf line 18
Adding compatibility with celery 5.0.6 which have different worker 'run' command
---
extra/supervisord/celeryd.conf | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/extra/supervisord/celeryd.conf b/extra/supervisord/celeryd.conf
index 2668ccb4c17..90254f7d4cd 100644
--- a/extra/supervisord/celeryd.conf
+++ b/extra/supervisord/celeryd.conf
@@ -15,7 +15,7 @@ autorestart=true
startsecs=10
; Set full path to celery program if using virtualenv
-command=celery worker -A proj --loglevel=INFO
+command=celery -A proj worker --loglevel=INFO
; Alternatively,
;command=celery --app=your_app.celery:app worker --loglevel=INFO -n worker.%%h
From b37182855fab80a356ff41a2153c72c00cf045d9 Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Thu, 14 Jan 2021 23:26:05 +0100
Subject: [PATCH 142/415] Trace task optimizations are now set via Celery app
instance
---
celery/app/base.py | 4 +++
celery/app/trace.py | 16 ++++-------
celery/worker/request.py | 12 +++++---
celery/worker/strategy.py | 2 +-
t/unit/tasks/test_trace.py | 2 +-
t/unit/worker/test_request.py | 52 +++++++++++++++++++++++++++++++----
6 files changed, 66 insertions(+), 22 deletions(-)
diff --git a/celery/app/base.py b/celery/app/base.py
index 27e5b610ca7..d833fc1e0e6 100644
--- a/celery/app/base.py
+++ b/celery/app/base.py
@@ -302,6 +302,10 @@ def __init__(self, main=None, loader=None, backend=None,
self.on_after_finalize = Signal(name='app.on_after_finalize')
self.on_after_fork = Signal(name='app.on_after_fork')
+ # Boolean signalling, whether fast_trace_task are enabled.
+ # this attribute is set in celery.worker.trace and checked by celery.worker.request
+ self.use_fast_trace_task = False
+
self.on_init()
_register_app(self)
diff --git a/celery/app/trace.py b/celery/app/trace.py
index f9b8c83e6e6..28273250d92 100644
--- a/celery/app/trace.py
+++ b/celery/app/trace.py
@@ -606,6 +606,8 @@ def _fast_trace_task(task, uuid, request, body, content_type,
)
return (1, R, T) if I else (0, Rstr, T)
+fast_trace_task = _fast_trace_task # noqa: E305
+
def report_internal_error(task, exc):
_type, _value, _tb = sys.exc_info()
@@ -622,8 +624,6 @@ def report_internal_error(task, exc):
def setup_worker_optimizations(app, hostname=None):
"""Setup worker related optimizations."""
- global trace_task_ret
-
hostname = hostname or gethostname()
# make sure custom Task.__call__ methods that calls super
@@ -649,16 +649,11 @@ def setup_worker_optimizations(app, hostname=None):
hostname,
]
- trace_task_ret = _fast_trace_task
- from celery.worker import request as request_module
- request_module.trace_task_ret = _fast_trace_task
- request_module.__optimize__()
+ app.use_fast_trace_task = True
-def reset_worker_optimizations():
+def reset_worker_optimizations(app):
"""Reset previously configured optimizations."""
- global trace_task_ret
- trace_task_ret = _trace_task_ret
try:
delattr(BaseTask, '_stackprotected')
except AttributeError:
@@ -667,8 +662,7 @@ def reset_worker_optimizations():
BaseTask.__call__ = _patched.pop('BaseTask.__call__')
except KeyError:
pass
- from celery.worker import request as request_module
- request_module.trace_task_ret = _trace_task_ret
+ app.use_fast_trace_task = False
def _install_stack_protection():
diff --git a/celery/worker/request.py b/celery/worker/request.py
index 81c3387d98a..71ed7192137 100644
--- a/celery/worker/request.py
+++ b/celery/worker/request.py
@@ -15,7 +15,7 @@
from celery import signals
from celery.app.task import Context
-from celery.app.trace import trace_task, trace_task_ret
+from celery.app.trace import trace_task, trace_task_ret, fast_trace_task
from celery.exceptions import (Ignore, InvalidTaskError, Reject, Retry,
TaskRevokedError, Terminated,
TimeLimitExceeded, WorkerLostError)
@@ -323,8 +323,9 @@ def execute_using_pool(self, pool, **kwargs):
raise TaskRevokedError(task_id)
time_limit, soft_time_limit = self.time_limits
+ trace = fast_trace_task if self._app.use_fast_trace_task else trace_task_ret
result = pool.apply_async(
- trace_task_ret,
+ trace,
args=(self._type, task_id, self._request_dict, self._body,
self._content_type, self._content_encoding),
accept_callback=self.on_accepted,
@@ -627,15 +628,18 @@ def group_index(self):
return self._request_dict.get('group_index')
-def create_request_cls(base, task, pool, hostname, eventer,
+def create_request_cls(app, base, task, pool, hostname, eventer,
ref=ref, revoked_tasks=revoked_tasks,
- task_ready=task_ready, trace=trace_task_ret):
+ task_ready=task_ready, trace=None):
default_time_limit = task.time_limit
default_soft_time_limit = task.soft_time_limit
apply_async = pool.apply_async
acks_late = task.acks_late
events = eventer and eventer.enabled
+ if trace is None:
+ trace = fast_trace_task if app.use_fast_trace_task else trace_task_ret
+
class Request(base):
def execute_using_pool(self, pool, **kwargs):
diff --git a/celery/worker/strategy.py b/celery/worker/strategy.py
index 8fb1eabd319..6adc3b82c64 100644
--- a/celery/worker/strategy.py
+++ b/celery/worker/strategy.py
@@ -124,7 +124,7 @@ def default(task, app, consumer,
limit_task = consumer._limit_task
limit_post_eta = consumer._limit_post_eta
Request = symbol_by_name(task.Request)
- Req = create_request_cls(Request, task, consumer.pool, hostname, eventer)
+ Req = create_request_cls(app, Request, task, consumer.pool, hostname, eventer)
revoked_tasks = consumer.controller.state.revoked
diff --git a/t/unit/tasks/test_trace.py b/t/unit/tasks/test_trace.py
index 3d7061acea5..0b6fd4196ce 100644
--- a/t/unit/tasks/test_trace.py
+++ b/t/unit/tasks/test_trace.py
@@ -435,4 +435,4 @@ def foo(self, i):
assert foo(1).called_directly
finally:
- reset_worker_optimizations()
+ reset_worker_optimizations(self.app)
diff --git a/t/unit/worker/test_request.py b/t/unit/worker/test_request.py
index c0d0119d9b8..243ea3ac6d0 100644
--- a/t/unit/worker/test_request.py
+++ b/t/unit/worker/test_request.py
@@ -762,8 +762,9 @@ def test_on_soft_timeout(self, patching):
def test_fast_trace_task(self):
from celery.app import trace
+ assert self.app.use_fast_trace_task is False
setup_worker_optimizations(self.app)
- assert trace.trace_task_ret is trace._fast_trace_task
+ assert self.app.use_fast_trace_task is True
tid = uuid()
message = self.TaskMessage(self.mytask.name, tid, args=[4])
assert len(message.payload) == 3
@@ -772,7 +773,7 @@ def test_fast_trace_task(self):
self.mytask.name, self.mytask, self.app.loader, 'test',
app=self.app,
)
- failed, res, runtime = trace.trace_task_ret(
+ failed, res, runtime = trace.fast_trace_task(
self.mytask.name, tid, message.headers, message.body,
message.content_type, message.content_encoding)
assert not failed
@@ -780,8 +781,8 @@ def test_fast_trace_task(self):
assert runtime is not None
assert isinstance(runtime, numbers.Real)
finally:
- reset_worker_optimizations()
- assert trace.trace_task_ret is trace._trace_task_ret
+ reset_worker_optimizations(self.app)
+ assert self.app.use_fast_trace_task is False
delattr(self.mytask, '__trace__')
failed, res, runtime = trace.trace_task_ret(
self.mytask.name, tid, message.headers, message.body,
@@ -977,11 +978,30 @@ def test_execute_fail(self):
assert isinstance(meta['result'], KeyError)
def test_execute_using_pool(self):
+ from celery.app.trace import trace_task_ret
tid = uuid()
job = self.xRequest(id=tid, args=[4])
p = Mock()
job.execute_using_pool(p)
p.apply_async.assert_called_once()
+ trace = p.apply_async.call_args[0][0]
+ assert trace == trace_task_ret
+ args = p.apply_async.call_args[1]['args']
+ assert args[0] == self.mytask.name
+ assert args[1] == tid
+ assert args[2] == job.request_dict
+ assert args[3] == job.message.body
+
+ def test_execute_using_pool_fast_trace_task(self):
+ from celery.app.trace import fast_trace_task
+ self.app.use_fast_trace_task = True
+ tid = uuid()
+ job = self.xRequest(id=tid, args=[4])
+ p = Mock()
+ job.execute_using_pool(p)
+ p.apply_async.assert_called_once()
+ trace = p.apply_async.call_args[0][0]
+ assert trace == fast_trace_task
args = p.apply_async.call_args[1]['args']
assert args[0] == self.mytask.name
assert args[1] == tid
@@ -1054,7 +1074,7 @@ def setup(self):
def create_request_cls(self, **kwargs):
return create_request_cls(
- Request, self.task, self.pool, 'foo', self.eventer, **kwargs
+ self.app, Request, self.task, self.pool, 'foo', self.eventer, **kwargs
)
def zRequest(self, Request=None, revoked_tasks=None, ref=None, **kwargs):
@@ -1153,6 +1173,28 @@ def test_execute_using_pool(self):
weakref_ref.assert_called_with(self.pool.apply_async())
assert job._apply_result is weakref_ref()
+ def test_execute_using_pool_with_use_fast_trace_task(self):
+ from celery.app.trace import fast_trace_task as trace
+ self.app.use_fast_trace_task = True
+ weakref_ref = Mock(name='weakref.ref')
+ job = self.zRequest(id=uuid(), revoked_tasks=set(), ref=weakref_ref)
+ job.execute_using_pool(self.pool)
+ self.pool.apply_async.assert_called_with(
+ trace,
+ args=(job.type, job.id, job.request_dict, job.body,
+ job.content_type, job.content_encoding),
+ accept_callback=job.on_accepted,
+ timeout_callback=job.on_timeout,
+ callback=job.on_success,
+ error_callback=job.on_failure,
+ soft_timeout=self.task.soft_time_limit,
+ timeout=self.task.time_limit,
+ correlation_id=job.id,
+ )
+ assert job._apply_result
+ weakref_ref.assert_called_with(self.pool.apply_async())
+ assert job._apply_result is weakref_ref()
+
def test_execute_using_pool_with_none_timelimit_header(self):
from celery.app.trace import trace_task_ret as trace
weakref_ref = Mock(name='weakref.ref')
From 3af6d9d5e3f52556a63e8091ee777890672256f4 Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Fri, 15 Jan 2021 16:14:29 +0100
Subject: [PATCH 143/415] Make trace_task_ret and fast_trace_task public
---
celery/app/trace.py | 17 ++++++-----------
t/unit/tasks/test_trace.py | 6 +++---
t/unit/worker/test_request.py | 25 ++++++++++---------------
3 files changed, 19 insertions(+), 29 deletions(-)
diff --git a/celery/app/trace.py b/celery/app/trace.py
index 28273250d92..82a4957b2ef 100644
--- a/celery/app/trace.py
+++ b/celery/app/trace.py
@@ -560,9 +560,9 @@ def _signal_internal_error(task, uuid, args, kwargs, request, exc):
del tb
-def _trace_task_ret(name, uuid, request, body, content_type,
- content_encoding, loads=loads_message, app=None,
- **extra_request):
+def trace_task_ret(name, uuid, request, body, content_type,
+ content_encoding, loads=loads_message, app=None,
+ **extra_request):
app = app or current_app._get_current_object()
embed = None
if content_type:
@@ -582,12 +582,9 @@ def _trace_task_ret(name, uuid, request, body, content_type,
return (1, R, T) if I else (0, Rstr, T)
-trace_task_ret = _trace_task_ret # noqa: E305
-
-
-def _fast_trace_task(task, uuid, request, body, content_type,
- content_encoding, loads=loads_message, _loc=None,
- hostname=None, **_):
+def fast_trace_task(task, uuid, request, body, content_type,
+ content_encoding, loads=loads_message, _loc=None,
+ hostname=None, **_):
_loc = _localized if not _loc else _loc
embed = None
tasks, accept, hostname = _loc
@@ -606,8 +603,6 @@ def _fast_trace_task(task, uuid, request, body, content_type,
)
return (1, R, T) if I else (0, Rstr, T)
-fast_trace_task = _fast_trace_task # noqa: E305
-
def report_internal_error(task, exc):
_type, _value, _tb = sys.exc_info()
diff --git a/t/unit/tasks/test_trace.py b/t/unit/tasks/test_trace.py
index 0b6fd4196ce..cb26720aedc 100644
--- a/t/unit/tasks/test_trace.py
+++ b/t/unit/tasks/test_trace.py
@@ -6,7 +6,7 @@
from celery import group, signals, states, uuid
from celery.app.task import Context
-from celery.app.trace import (TraceInfo, _fast_trace_task, _trace_task_ret,
+from celery.app.trace import (TraceInfo, fast_trace_task, trace_task_ret,
build_tracer, get_log_policy, get_task_name,
log_policy_expected, log_policy_ignore,
log_policy_internal, log_policy_reject,
@@ -336,7 +336,7 @@ def test_trace_exception(self, mock_traceback_clear):
mock_traceback_clear.assert_called()
def test_trace_task_ret__no_content_type(self):
- _trace_task_ret(
+ trace_task_ret(
self.add.name, 'id1', {}, ((2, 2), {}, {}), None, None, app=self.app,
)
@@ -344,7 +344,7 @@ def test_fast_trace_task__no_content_type(self):
self.app.tasks[self.add.name].__trace__ = build_tracer(
self.add.name, self.add, app=self.app,
)
- _fast_trace_task(
+ fast_trace_task(
self.add.name,
'id1',
{},
diff --git a/t/unit/worker/test_request.py b/t/unit/worker/test_request.py
index 243ea3ac6d0..f6d5f97c974 100644
--- a/t/unit/worker/test_request.py
+++ b/t/unit/worker/test_request.py
@@ -12,9 +12,10 @@
from kombu.utils.uuid import uuid
from celery import states
-from celery.app.trace import (TraceInfo, _trace_task_ret, build_tracer,
+from celery.app.trace import (TraceInfo, trace_task_ret, build_tracer,
mro_lookup, reset_worker_optimizations,
- setup_worker_optimizations, trace_task)
+ setup_worker_optimizations, trace_task,
+ fast_trace_task)
from celery.backends.base import BaseDictBackend
from celery.exceptions import (Ignore, InvalidTaskError, Reject, Retry,
TaskRevokedError, Terminated, WorkerLostError)
@@ -761,7 +762,6 @@ def test_on_soft_timeout(self, patching):
assert self.mytask.backend.get_status(job.id) == states.PENDING
def test_fast_trace_task(self):
- from celery.app import trace
assert self.app.use_fast_trace_task is False
setup_worker_optimizations(self.app)
assert self.app.use_fast_trace_task is True
@@ -773,7 +773,7 @@ def test_fast_trace_task(self):
self.mytask.name, self.mytask, self.app.loader, 'test',
app=self.app,
)
- failed, res, runtime = trace.fast_trace_task(
+ failed, res, runtime = fast_trace_task(
self.mytask.name, tid, message.headers, message.body,
message.content_type, message.content_encoding)
assert not failed
@@ -784,7 +784,7 @@ def test_fast_trace_task(self):
reset_worker_optimizations(self.app)
assert self.app.use_fast_trace_task is False
delattr(self.mytask, '__trace__')
- failed, res, runtime = trace.trace_task_ret(
+ failed, res, runtime = trace_task_ret(
self.mytask.name, tid, message.headers, message.body,
message.content_type, message.content_encoding, app=self.app,
)
@@ -800,7 +800,7 @@ def test_trace_task_ret(self):
)
tid = uuid()
message = self.TaskMessage(self.mytask.name, tid, args=[4])
- _, R, _ = _trace_task_ret(
+ _, R, _ = trace_task_ret(
self.mytask.name, tid, message.headers,
message.body, message.content_type,
message.content_encoding, app=self.app,
@@ -814,7 +814,7 @@ def test_trace_task_ret__no_trace(self):
pass
tid = uuid()
message = self.TaskMessage(self.mytask.name, tid, args=[4])
- _, R, _ = _trace_task_ret(
+ _, R, _ = trace_task_ret(
self.mytask.name, tid, message.headers,
message.body, message.content_type,
message.content_encoding, app=self.app,
@@ -978,7 +978,6 @@ def test_execute_fail(self):
assert isinstance(meta['result'], KeyError)
def test_execute_using_pool(self):
- from celery.app.trace import trace_task_ret
tid = uuid()
job = self.xRequest(id=tid, args=[4])
p = Mock()
@@ -993,7 +992,6 @@ def test_execute_using_pool(self):
assert args[3] == job.message.body
def test_execute_using_pool_fast_trace_task(self):
- from celery.app.trace import fast_trace_task
self.app.use_fast_trace_task = True
tid = uuid()
job = self.xRequest(id=tid, args=[4])
@@ -1153,12 +1151,11 @@ def test_execute_using_pool__expired(self):
job.execute_using_pool(self.pool)
def test_execute_using_pool(self):
- from celery.app.trace import trace_task_ret as trace
weakref_ref = Mock(name='weakref.ref')
job = self.zRequest(id=uuid(), revoked_tasks=set(), ref=weakref_ref)
job.execute_using_pool(self.pool)
self.pool.apply_async.assert_called_with(
- trace,
+ trace_task_ret,
args=(job.type, job.id, job.request_dict, job.body,
job.content_type, job.content_encoding),
accept_callback=job.on_accepted,
@@ -1174,13 +1171,12 @@ def test_execute_using_pool(self):
assert job._apply_result is weakref_ref()
def test_execute_using_pool_with_use_fast_trace_task(self):
- from celery.app.trace import fast_trace_task as trace
self.app.use_fast_trace_task = True
weakref_ref = Mock(name='weakref.ref')
job = self.zRequest(id=uuid(), revoked_tasks=set(), ref=weakref_ref)
job.execute_using_pool(self.pool)
self.pool.apply_async.assert_called_with(
- trace,
+ fast_trace_task,
args=(job.type, job.id, job.request_dict, job.body,
job.content_type, job.content_encoding),
accept_callback=job.on_accepted,
@@ -1196,7 +1192,6 @@ def test_execute_using_pool_with_use_fast_trace_task(self):
assert job._apply_result is weakref_ref()
def test_execute_using_pool_with_none_timelimit_header(self):
- from celery.app.trace import trace_task_ret as trace
weakref_ref = Mock(name='weakref.ref')
job = self.zRequest(id=uuid(),
revoked_tasks=set(),
@@ -1204,7 +1199,7 @@ def test_execute_using_pool_with_none_timelimit_header(self):
headers={'timelimit': None})
job.execute_using_pool(self.pool)
self.pool.apply_async.assert_called_with(
- trace,
+ trace_task_ret,
args=(job.type, job.id, job.request_dict, job.body,
job.content_type, job.content_encoding),
accept_callback=job.on_accepted,
From 948bb797d0975721aa41564fcec47eb462483c71 Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Fri, 15 Jan 2021 17:05:13 +0100
Subject: [PATCH 144/415] reset_worker_optimizations and create_request_cls has
now app as optional parameter
---
celery/app/trace.py | 2 +-
celery/worker/request.py | 6 +++---
celery/worker/strategy.py | 2 +-
t/unit/worker/test_request.py | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/celery/app/trace.py b/celery/app/trace.py
index 82a4957b2ef..e43152afc6d 100644
--- a/celery/app/trace.py
+++ b/celery/app/trace.py
@@ -647,7 +647,7 @@ def setup_worker_optimizations(app, hostname=None):
app.use_fast_trace_task = True
-def reset_worker_optimizations(app):
+def reset_worker_optimizations(app=current_app):
"""Reset previously configured optimizations."""
try:
delattr(BaseTask, '_stackprotected')
diff --git a/celery/worker/request.py b/celery/worker/request.py
index 71ed7192137..a5721eccdb2 100644
--- a/celery/worker/request.py
+++ b/celery/worker/request.py
@@ -13,7 +13,7 @@
from kombu.utils.encoding import safe_repr, safe_str
from kombu.utils.objects import cached_property
-from celery import signals
+from celery import signals, current_app
from celery.app.task import Context
from celery.app.trace import trace_task, trace_task_ret, fast_trace_task
from celery.exceptions import (Ignore, InvalidTaskError, Reject, Retry,
@@ -628,9 +628,9 @@ def group_index(self):
return self._request_dict.get('group_index')
-def create_request_cls(app, base, task, pool, hostname, eventer,
+def create_request_cls(base, task, pool, hostname, eventer,
ref=ref, revoked_tasks=revoked_tasks,
- task_ready=task_ready, trace=None):
+ task_ready=task_ready, trace=None, app=current_app):
default_time_limit = task.time_limit
default_soft_time_limit = task.soft_time_limit
apply_async = pool.apply_async
diff --git a/celery/worker/strategy.py b/celery/worker/strategy.py
index 6adc3b82c64..98a47015352 100644
--- a/celery/worker/strategy.py
+++ b/celery/worker/strategy.py
@@ -124,7 +124,7 @@ def default(task, app, consumer,
limit_task = consumer._limit_task
limit_post_eta = consumer._limit_post_eta
Request = symbol_by_name(task.Request)
- Req = create_request_cls(app, Request, task, consumer.pool, hostname, eventer)
+ Req = create_request_cls(Request, task, consumer.pool, hostname, eventer, app=app)
revoked_tasks = consumer.controller.state.revoked
diff --git a/t/unit/worker/test_request.py b/t/unit/worker/test_request.py
index f6d5f97c974..9650547bb57 100644
--- a/t/unit/worker/test_request.py
+++ b/t/unit/worker/test_request.py
@@ -1072,7 +1072,7 @@ def setup(self):
def create_request_cls(self, **kwargs):
return create_request_cls(
- self.app, Request, self.task, self.pool, 'foo', self.eventer, **kwargs
+ Request, self.task, self.pool, 'foo', self.eventer, app=self.app, **kwargs
)
def zRequest(self, Request=None, revoked_tasks=None, ref=None, **kwargs):
From a5357cab8aa80ff701a1970f55dc1e1083a161f5 Mon Sep 17 00:00:00 2001
From: pavlos kallis
Date: Sun, 14 Feb 2021 16:16:04 +0200
Subject: [PATCH 145/415] Small refactor (#6633)
Co-authored-by: Pavlos Kallis
---
celery/worker/request.py | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/celery/worker/request.py b/celery/worker/request.py
index a5721eccdb2..cb56936e2f5 100644
--- a/celery/worker/request.py
+++ b/celery/worker/request.py
@@ -485,16 +485,15 @@ def on_retry(self, exc_info):
def on_failure(self, exc_info, send_failed_event=True, return_ok=False):
"""Handler called if the task raised an exception."""
task_ready(self)
- if isinstance(exc_info.exception, MemoryError):
- raise MemoryError(f'Process got: {exc_info.exception}')
- elif isinstance(exc_info.exception, Reject):
- return self.reject(requeue=exc_info.exception.requeue)
- elif isinstance(exc_info.exception, Ignore):
- return self.acknowledge()
-
exc = exc_info.exception
- if isinstance(exc, Retry):
+ if isinstance(exc, MemoryError):
+ raise MemoryError(f'Process got: {exc}')
+ elif isinstance(exc, Reject):
+ return self.reject(requeue=exc.requeue)
+ elif isinstance(exc, Ignore):
+ return self.acknowledge()
+ elif isinstance(exc, Retry):
return self.on_retry(exc_info)
# (acks_late) acknowledge after result stored.
From 9e44ddb2ccc5e7df8b3513e54f75d4f1f03a19a7 Mon Sep 17 00:00:00 2001
From: Kostya Deev
Date: Sun, 24 Jan 2021 17:34:16 -0600
Subject: [PATCH 146/415] Fix for issue #5030 "Celery Result backend on Windows
OS".
---
celery/backends/filesystem.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/celery/backends/filesystem.py b/celery/backends/filesystem.py
index 6b937b693b5..26a48aeaa56 100644
--- a/celery/backends/filesystem.py
+++ b/celery/backends/filesystem.py
@@ -38,6 +38,10 @@ def __init__(self, url=None, open=open, unlink=os.unlink, sep=os.sep,
self.url = url
path = self._find_path(url)
+ # Remove forwarding "/" for Windows os
+ if os.name == "nt" and path.startswith("/"):
+ path = path[1:]
+
# We need the path and separator as bytes objects
self.path = path.encode(encoding)
self.sep = sep.encode(encoding)
From 6eb5b718843d69e31bb2c90e3efa2e2aa39f5f94 Mon Sep 17 00:00:00 2001
From: Fahmi
Date: Fri, 19 Feb 2021 05:44:46 +0700
Subject: [PATCH 147/415] Fixed a typo on copyright section.
---
celery/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/celery/__init__.py b/celery/__init__.py
index ae3388c0e56..33c9902ba08 100644
--- a/celery/__init__.py
+++ b/celery/__init__.py
@@ -1,5 +1,5 @@
"""Distributed Task Queue."""
-# :copyright: (c) 2016-20206 Asif Saif Uddin, celery core and individual
+# :copyright: (c) 2016-2026 Asif Saif Uddin, celery core and individual
# contributors, All rights reserved.
# :copyright: (c) 2015-2016 Ask Solem. All rights reserved.
# :copyright: (c) 2012-2014 GoPivotal, Inc., All rights reserved.
From 1f2d9d9abe4b34c1b6fa3a890aab70e3611a3021 Mon Sep 17 00:00:00 2001
From: "Asif Saif Uddin (Auvi)"
Date: Fri, 19 Feb 2021 11:21:55 +0600
Subject: [PATCH 148/415] update next-steps setup
---
examples/next-steps/setup.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/examples/next-steps/setup.py b/examples/next-steps/setup.py
index 8d9415cbd29..50449e59934 100644
--- a/examples/next-steps/setup.py
+++ b/examples/next-steps/setup.py
@@ -14,26 +14,26 @@
author='Ola A. Normann',
author_email='author@example.com',
keywords='our celery integration',
- version='1.0',
+ version='2.0',
description='Tasks for my project',
long_description=__doc__,
license='BSD',
packages=find_packages(exclude=['ez_setup', 'tests', 'tests.*']),
- test_suite='nose.collector',
+ test_suite='pytest',
zip_safe=False,
install_requires=[
- 'celery>=4.0',
+ 'celery>=5.0',
# 'requests',
],
classifiers=[
'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: BSD License',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: Implementation :: CPython',
- 'Programming Language :: Python :: Implementation :: PyPy',
+ 'Programming Language :: Python :: Implementation :: PyPy3',
'Operating System :: OS Independent',
],
)
From 1eade4c81bb5cd4715cf9269c1dfc806a472fa13 Mon Sep 17 00:00:00 2001
From: "Asif Saif Uddin (Auvi)"
Date: Fri, 19 Feb 2021 11:29:15 +0600
Subject: [PATCH 149/415] update django exaples
---
examples/django/proj/urls.py | 2 +-
examples/django/requirements.txt | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/examples/django/proj/urls.py b/examples/django/proj/urls.py
index 2616749dd6e..5f67c27b660 100644
--- a/examples/django/proj/urls.py
+++ b/examples/django/proj/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import handler404, handler500, include, url # noqa
+from django.urls import handler404, handler500, include, url # noqa
# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
diff --git a/examples/django/requirements.txt b/examples/django/requirements.txt
index 72e653a9d83..4ba37fb5b8a 100644
--- a/examples/django/requirements.txt
+++ b/examples/django/requirements.txt
@@ -1,3 +1,3 @@
-django>=2.0.0
+django>=2.2.1
sqlalchemy>=1.0.14
-celery>=4.3.0
+celery>=5.0.5
From 33849376578986af13807d829260356da71e4e93 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 13 Jan 2021 10:40:51 +0200
Subject: [PATCH 150/415] isort.
---
celery/backends/redis.py | 3 ++-
celery/worker/request.py | 4 ++--
t/integration/test_inspect.py | 2 +-
t/unit/backends/test_redis.py | 3 ++-
t/unit/tasks/test_trace.py | 6 +++---
t/unit/utils/test_functional.py | 3 ++-
t/unit/worker/test_request.py | 4 ++--
7 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index a0d392d9527..3ffdf70aa3d 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -12,7 +12,8 @@
from celery import states
from celery._state import task_join_will_block
from celery.canvas import maybe_signature
-from celery.exceptions import BackendStoreError, ChordError, ImproperlyConfigured
+from celery.exceptions import (BackendStoreError, ChordError,
+ ImproperlyConfigured)
from celery.result import GroupResult, allow_join_result
from celery.utils.functional import dictfilter
from celery.utils.log import get_logger
diff --git a/celery/worker/request.py b/celery/worker/request.py
index cb56936e2f5..c1847820aae 100644
--- a/celery/worker/request.py
+++ b/celery/worker/request.py
@@ -13,9 +13,9 @@
from kombu.utils.encoding import safe_repr, safe_str
from kombu.utils.objects import cached_property
-from celery import signals, current_app
+from celery import current_app, signals
from celery.app.task import Context
-from celery.app.trace import trace_task, trace_task_ret, fast_trace_task
+from celery.app.trace import fast_trace_task, trace_task, trace_task_ret
from celery.exceptions import (Ignore, InvalidTaskError, Reject, Retry,
TaskRevokedError, Terminated,
TimeLimitExceeded, WorkerLostError)
diff --git a/t/integration/test_inspect.py b/t/integration/test_inspect.py
index 6070de483d2..60332f0071d 100644
--- a/t/integration/test_inspect.py
+++ b/t/integration/test_inspect.py
@@ -94,7 +94,7 @@ def test_active_queues(self, inspect):
'no_declare': None,
'queue_arguments': None,
'routing_key': 'celery'}
- ]
+ ]
@flaky
def test_active(self, inspect):
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index 23580fa3dfb..75d917b5cef 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -12,7 +12,8 @@
from celery import signature, states, uuid
from celery.canvas import Signature
-from celery.exceptions import BackendStoreError, ChordError, ImproperlyConfigured
+from celery.exceptions import (BackendStoreError, ChordError,
+ ImproperlyConfigured)
from celery.utils.collections import AttributeDict
diff --git a/t/unit/tasks/test_trace.py b/t/unit/tasks/test_trace.py
index cb26720aedc..81195439173 100644
--- a/t/unit/tasks/test_trace.py
+++ b/t/unit/tasks/test_trace.py
@@ -6,14 +6,14 @@
from celery import group, signals, states, uuid
from celery.app.task import Context
-from celery.app.trace import (TraceInfo, fast_trace_task, trace_task_ret,
- build_tracer, get_log_policy, get_task_name,
+from celery.app.trace import (TraceInfo, build_tracer, fast_trace_task,
+ get_log_policy, get_task_name,
log_policy_expected, log_policy_ignore,
log_policy_internal, log_policy_reject,
log_policy_unexpected,
reset_worker_optimizations,
setup_worker_optimizations, trace_task,
- traceback_clear)
+ trace_task_ret, traceback_clear)
from celery.backends.base import BaseDictBackend
from celery.exceptions import Ignore, Reject, Retry
diff --git a/t/unit/utils/test_functional.py b/t/unit/utils/test_functional.py
index 0eead299908..2100b074000 100644
--- a/t/unit/utils/test_functional.py
+++ b/t/unit/utils/test_functional.py
@@ -1,10 +1,11 @@
import pytest
+from kombu.utils.functional import lazy
+
from celery.utils.functional import (DummyContext, first, firstmethod,
fun_accepts_kwargs, fun_takes_argument,
head_from_fun, maybe_list, mlazy,
padlist, regen, seq_concat_item,
seq_concat_seq)
-from kombu.utils.functional import lazy
def test_DummyContext():
diff --git a/t/unit/worker/test_request.py b/t/unit/worker/test_request.py
index 9650547bb57..013cdf01aea 100644
--- a/t/unit/worker/test_request.py
+++ b/t/unit/worker/test_request.py
@@ -12,10 +12,10 @@
from kombu.utils.uuid import uuid
from celery import states
-from celery.app.trace import (TraceInfo, trace_task_ret, build_tracer,
+from celery.app.trace import (TraceInfo, build_tracer, fast_trace_task,
mro_lookup, reset_worker_optimizations,
setup_worker_optimizations, trace_task,
- fast_trace_task)
+ trace_task_ret)
from celery.backends.base import BaseDictBackend
from celery.exceptions import (Ignore, InvalidTaskError, Reject, Retry,
TaskRevokedError, Terminated, WorkerLostError)
From 015442278d33622122008e70f21b004f318aaf52 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 23 Feb 2021 13:47:19 +0200
Subject: [PATCH 151/415] Report code coverage using codecov. (#6642)
---
.github/workflows/python-package.yml | 12 ++-
.travis.yml | 140 ---------------------------
2 files changed, 9 insertions(+), 143 deletions(-)
delete mode 100644 .travis.yml
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 414522b8dc9..f6558ba8334 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -43,13 +43,13 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- python -m pip install flake8 pytest case pytest-celery pytest-subtests pytest-timeout
+ python -m pip install flake8 pytest case pytest-celery pytest-subtests pytest-timeout pytest-cov
python -m pip install moto boto3 msgpack PyYAML
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
-
+
- name: Run Unit test with pytest
run: |
- PYTHONPATH=. pytest -xv t/unit
+ PYTHONPATH=. pytest -xv --cov=celery --cov-report=xml --cov-report term t/unit
- name: Lint with flake8
run: |
@@ -57,3 +57,9 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+ - uses: codecov/codecov-action@v1
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
+ flags: unittests # optional
+ fail_ci_if_error: true # optional (default = false)
+ verbose: true # optional (default = false)
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 316d206de11..00000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,140 +0,0 @@
-language: python
-dist: bionic
-cache: pip
-python:
- - '3.6'
- - '3.7'
- - '3.8'
- - '3.9'
-os:
- - linux
-stages:
- - test
- - integration
- - lint
-services:
- - redis
- - docker
-env:
- global:
- - PYTHONUNBUFFERED=yes
- - CELERY_TOX_PARALLEL=
- jobs:
- - MATRIX_TOXENV=unit
-
-jobs:
- fast_finish: true
- allow_failures:
- - python: '3.9'
- include:
- - python: '3.9'
- env: MATRIX_TOXENV=integration-rabbitmq
- stage: integration
-
- - python: 3.8
- env: MATRIX_TOXENV=integration-rabbitmq
- stage: integration
-
- - python: 3.8
- env: MATRIX_TOXENV=integration-redis
- stage: integration
-
- - python: 3.8
- env: MATRIX_TOXENV=integration-dynamodb
- stage: integration
-
- - python: 3.8
- env: MATRIX_TOXENV=integration-azureblockblob
- stage: integration
-
- - python: 3.8
- env: MATRIX_TOXENV=integration-cache
- stage: integration
-
- - python: 3.8
- env: MATRIX_TOXENV=integration-cassandra
- stage: integration
-
- - python: 3.8
- env: MATRIX_TOXENV=integration-elasticsearch
- stage: integration
-
- - python: '3.9'
- env:
- - TOXENV=flake8,apicheck,configcheck,bandit
- - CELERY_TOX_PARALLEL='--parallel --parallel-live'
- stage: lint
-
- - python: pypy3.6-7.3.1
- env: TOXENV=pypy3-unit
- stage: test
-
-before_install:
- - sudo install --directory --owner=travis /var/log/celery /var/run/celery
- - sudo apt install libcurl4-openssl-dev libssl-dev gnutls-dev httping expect
- - if [[ -v MATRIX_TOXENV ]]; then export TOXENV=${TRAVIS_PYTHON_VERSION}-${MATRIX_TOXENV}; fi; env
- - |
- if [[ "$TOXENV" == *rabbitmq ]]; then
- docker run -d -p 5672:5672 -p 15672:15672 rabbitmq:3.8-management
- while ! httping -c1 http://127.0.0.1:15672; do sleep 10; done
- fi
- - |
- if [[ "$TOXENV" =~ "pypy" ]]; then
- export PYENV_ROOT="$HOME/.pyenv"
- if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
- cd "$PYENV_ROOT" && git pull
- else
- rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/pyenv/pyenv.git "$PYENV_ROOT"
- fi
- "$PYENV_ROOT/bin/pyenv" install "$PYPY_VERSION"
- virtualenv --python="$PYENV_ROOT/versions/$PYPY_VERSION/bin/python" "$HOME/virtualenvs/$PYPY_VERSION"
- source "$HOME/virtualenvs/$PYPY_VERSION/bin/activate"
- which python
- fi
- - |
- if [[ "$TOXENV" == *dynamodb ]]; then
- docker run -d -p 8000:8000 amazon/dynamodb-local
- while ! httping -c1 http://127.0.0.1:8000; do sleep 10; done
- fi
- - |
- if [[ "$TOXENV" == *cache ]]; then
- docker run -d -p 11211:11211 memcached:alpine
- while ! ./extra/travis/is-memcached-running 127.0.0.1 11211; do sleep 1; done
- fi
- - |
- if [[ "$TOXENV" == *cassandra ]]; then
- cassandra_container_id=$(sudo docker run -d -p 9042:9042 cassandra:latest)
- sudo docker exec $cassandra_container_id /bin/bash -c "while ! cqlsh -e 'describe cluster'; do sleep 1; done"
- sudo docker exec $cassandra_container_id /opt/cassandra/bin/cqlsh -e "CREATE KEYSPACE tests WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"
- sleep 1
- sudo docker exec $cassandra_container_id /opt/cassandra/bin/cqlsh -k tests -e "CREATE TABLE tests (task_id text, status text, result blob, date_done timestamp, traceback blob, children blob, PRIMARY KEY ((task_id), date_done)) WITH CLUSTERING ORDER BY (date_done DESC);"
- sleep 1
- fi
- - |
- if [[ "$TOXENV" == *elasticsearch ]]; then
- elasticsearch_container_id=$(sudo docker run -d -p 9200:9200 -e discovery.type=single-node elasticsearch:7.7.0)
- sudo docker exec $elasticsearch_container_id /bin/bash -c "while ! curl '127.0.0.1:9200/_cluster/health?wait_for_status=yellow&timeout=30s'; do sleep 1; done"
- fi
- - |
- docker run -d -e executable=blob -t -p 10000:10000 --tmpfs /opt/azurite/folder:rw arafato/azurite:2.6.5
- while ! httping -c1 http://127.0.0.1:10000; do sleep 10; done
- export AZUREBLOCKBLOB_URL="azureblockblob://DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
- - |
- wget -qO - https://packages.couchbase.com/ubuntu/couchbase.key | sudo apt-key add -
- sudo apt-add-repository -y 'deb http://packages.couchbase.com/ubuntu bionic bionic/main'
- sudo apt-get update && sudo apt-get install -y libcouchbase-dev
-install: pip --disable-pip-version-check install --upgrade-strategy eager -U tox | cat
-script: tox $CELERY_TOX_PARALLEL -v -- -v
-after_success:
- - |
- if [[ -v MATRIX_TOXENV || "$TOXENV" =~ "pypy" ]]; then
- .tox/$TOXENV/bin/coverage xml
- .tox/$TOXENV/bin/codecov -e TOXENV
- fi;
-notifications:
- email: false
- irc:
- channels:
- - "chat.freenode.net#celery"
- on_success: change
- on_failure: change
From e62dc67df607aefb84691d1d8cfb6a6e00ae26b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tomasz=20W=C3=B3jcik?=
Date: Wed, 24 Feb 2021 05:40:05 +0100
Subject: [PATCH 152/415] add store_eager_result setting so eager tasks can
store result on the backend (#6614)
* 6476 feat(setting): add setting `store_eager_result` that allows to store eagerly executed tasks results on the backend
* 6476 style(setting): fix flake8 and sphinx lint
* 6476 feat(results): add support for saving failed task results on the backend
* 6476 docs(results): reword new setting definition in docs, add myself to contributors
* 6476 docs(results): mention `task_store_eager_result` in docs 'testing' section where it's mention why/how use eager tasks when testing
* 6476 docs(results): add versionadded 5.1 in docs under task_store_eager_result
---
CONTRIBUTORS.txt | 1 +
celery/app/defaults.py | 1 +
celery/app/task.py | 1 +
celery/app/trace.py | 14 ++++-
docs/userguide/configuration.rst | 17 ++++++
docs/userguide/testing.rst | 3 +
t/unit/tasks/test_trace.py | 97 +++++++++++++++++++++++++++++++-
7 files changed, 131 insertions(+), 3 deletions(-)
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 2e27e625d43..7cf4b9a60bb 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -279,3 +279,4 @@ Sardorbek Imomaliev, 2020/01/24
Maksym Shalenyi, 2020/07/30
Frazer McLean, 2020/09/29
Henrik Bruåsdal, 2020/11/29
+Tom Wojcik, 2021/01/24
diff --git a/celery/app/defaults.py b/celery/app/defaults.py
index 9fec8472c96..51e1e2f96c1 100644
--- a/celery/app/defaults.py
+++ b/celery/app/defaults.py
@@ -255,6 +255,7 @@ def __repr__(self):
False, type='bool', old={'celery_eager_propagates_exceptions'},
),
ignore_result=Option(False, type='bool'),
+ store_eager_result=Option(False, type='bool'),
protocol=Option(2, type='int', old={'celery_task_protocol'}),
publish_retry=Option(
True, type='bool', old={'celery_task_publish_retry'},
diff --git a/celery/app/task.py b/celery/app/task.py
index 2265ebb9e67..5634c442152 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -309,6 +309,7 @@ class Task:
('acks_on_failure_or_timeout', 'task_acks_on_failure_or_timeout'),
('reject_on_worker_lost', 'task_reject_on_worker_lost'),
('ignore_result', 'task_ignore_result'),
+ ('store_eager_result', 'task_store_eager_result'),
('store_errors_even_if_ignored', 'task_store_errors_even_if_ignored'),
)
diff --git a/celery/app/trace.py b/celery/app/trace.py
index e43152afc6d..b6ff79fcef5 100644
--- a/celery/app/trace.py
+++ b/celery/app/trace.py
@@ -159,9 +159,13 @@ def __init__(self, state, retval=None):
def handle_error_state(self, task, req,
eager=False, call_errbacks=True):
- store_errors = not eager
if task.ignore_result:
store_errors = task.store_errors_even_if_ignored
+ elif eager and task.store_eager_result:
+ store_errors = True
+ else:
+ store_errors = not eager
+
return {
RETRY: self.handle_retry,
FAILURE: self.handle_failure,
@@ -316,7 +320,13 @@ def build_tracer(name, task, loader=None, hostname=None, store_errors=True,
ignore_result = task.ignore_result
track_started = task.track_started
track_started = not eager and (task.track_started and not ignore_result)
- publish_result = not eager and not ignore_result
+
+ # #6476
+ if eager and not ignore_result and task.store_eager_result:
+ publish_result = True
+ else:
+ publish_result = not eager and not ignore_result
+
hostname = hostname or gethostname()
inherit_parent_priority = app.conf.task_inherit_parent_priority
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index 01e8b7784e7..6e9c600b5f2 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -426,6 +426,23 @@ propagate exceptions.
It's the same as always running ``apply()`` with ``throw=True``.
+.. setting:: task_store_eager_result
+
+``task_store_eager_result``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 5.1
+
+Default: Disabled.
+
+If this is :const:`True` and :setting:`task_always_eager` is :const:`True`
+and :setting:`task_ignore_result` is :const:`False`,
+the results of eagerly executed tasks will be saved to the backend.
+
+By default, even with :setting:`task_always_eager` set to :const:`True`
+and :setting:`task_ignore_result` set to :const:`False`,
+the result will not be saved.
+
.. setting:: task_remote_tracebacks
``task_remote_tracebacks``
diff --git a/docs/userguide/testing.rst b/docs/userguide/testing.rst
index 94389c30739..62db0a21e41 100644
--- a/docs/userguide/testing.rst
+++ b/docs/userguide/testing.rst
@@ -18,6 +18,9 @@ To test task behavior in unit tests the preferred method is mocking.
of what happens in a worker, and there are many discrepancies
between the emulation and what happens in reality.
+ Note that eagerly executed tasks don't write results to backend by default.
+ If you want to enable this functionality, have a look at :setting:`task_store_eager_result`.
+
A Celery task is much like a web view, in that it should only
define how to perform the action in the context of being called as a task.
diff --git a/t/unit/tasks/test_trace.py b/t/unit/tasks/test_trace.py
index 81195439173..c7e11552976 100644
--- a/t/unit/tasks/test_trace.py
+++ b/t/unit/tasks/test_trace.py
@@ -1,4 +1,4 @@
-from unittest.mock import Mock, patch
+from unittest.mock import ANY, Mock, patch
import pytest
from billiard.einfo import ExceptionInfo
@@ -148,6 +148,75 @@ def add(x, y):
with pytest.raises(MemoryError):
self.trace(add, (2, 2), {}, eager=False)
+ def test_eager_task_does_not_store_result_even_if_not_ignore_result(self):
+ @self.app.task(shared=False)
+ def add(x, y):
+ return x + y
+
+ add.backend = Mock(name='backend')
+ add.ignore_result = False
+
+ self.trace(add, (2, 2), {}, eager=True)
+
+ add.backend.mark_as_done.assert_called_once_with(
+ 'id-1', # task_id
+ 4, # result
+ ANY, # request
+ False # store_result
+ )
+
+ def test_eager_task_does_not_call_store_result(self):
+ @self.app.task(shared=False)
+ def add(x, y):
+ return x + y
+
+ backend = BaseDictBackend(app=self.app)
+ backend.store_result = Mock()
+ add.backend = backend
+ add.ignore_result = False
+
+ self.trace(add, (2, 2), {}, eager=True)
+
+ add.backend.store_result.assert_not_called()
+
+ def test_eager_task_will_store_result_if_proper_setting_is_set(self):
+ @self.app.task(shared=False)
+ def add(x, y):
+ return x + y
+
+ add.backend = Mock(name='backend')
+ add.store_eager_result = True
+ add.ignore_result = False
+
+ self.trace(add, (2, 2), {}, eager=True)
+
+ add.backend.mark_as_done.assert_called_once_with(
+ 'id-1', # task_id
+ 4, # result
+ ANY, # request
+ True # store_result
+ )
+
+ def test_eager_task_with_setting_will_call_store_result(self):
+ @self.app.task(shared=False)
+ def add(x, y):
+ return x + y
+
+ backend = BaseDictBackend(app=self.app)
+ backend.store_result = Mock()
+ add.backend = backend
+ add.store_eager_result = True
+ add.ignore_result = False
+
+ self.trace(add, (2, 2), {}, eager=True)
+
+ add.backend.store_result.assert_called_once_with(
+ 'id-1',
+ 4,
+ states.SUCCESS,
+ request=ANY
+ )
+
def test_when_backend_raises_exception(self):
@self.app.task(shared=False)
def add(x, y):
@@ -413,6 +482,32 @@ def test_handle_error_state(self):
call_errbacks=True,
)
+ def test_handle_error_state_for_eager_task(self):
+ x = self.TI(states.FAILURE)
+ x.handle_failure = Mock()
+
+ x.handle_error_state(self.add, self.add.request, eager=True)
+ x.handle_failure.assert_called_once_with(
+ self.add,
+ self.add.request,
+ store_errors=False,
+ call_errbacks=True,
+ )
+
+ def test_handle_error_for_eager_saved_to_backend(self):
+ x = self.TI(states.FAILURE)
+ x.handle_failure = Mock()
+
+ self.add.store_eager_result = True
+
+ x.handle_error_state(self.add, self.add.request, eager=True)
+ x.handle_failure.assert_called_with(
+ self.add,
+ self.add.request,
+ store_errors=True,
+ call_errbacks=True,
+ )
+
@patch('celery.app.trace.ExceptionInfo')
def test_handle_reject(self, ExceptionInfo):
x = self.TI(states.FAILURE)
From ad3ec276e0ce5b18ccc545400c5ea1b04522cb72 Mon Sep 17 00:00:00 2001
From: Dani Hodovic
Date: Wed, 24 Feb 2021 07:18:22 +0200
Subject: [PATCH 153/415] Allow heartbeats to be sent in tests (#6632)
* Allow heartbeats to be sent in tests
I'm writing a Prometheus exporter at
https://github.com/danihodovic/celery-exporter.
In order to test the worker events: worker-heartbeat, worker-online,
worker-offline I need to be able to enable heartbeats for the testing
worker.
* Add docs on heartbeats in tests
---
celery/contrib/testing/worker.py | 2 +-
docs/userguide/testing.rst | 14 ++++++++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/celery/contrib/testing/worker.py b/celery/contrib/testing/worker.py
index 16d2582897d..09fecc0a7a2 100644
--- a/celery/contrib/testing/worker.py
+++ b/celery/contrib/testing/worker.py
@@ -119,7 +119,7 @@ def _start_worker_thread(app,
logfile=logfile,
# not allowed to override TestWorkController.on_consumer_ready
ready_callback=None,
- without_heartbeat=True,
+ without_heartbeat=kwargs.pop("without_heartbeat", True),
without_mingle=True,
without_gossip=True,
**kwargs)
diff --git a/docs/userguide/testing.rst b/docs/userguide/testing.rst
index 62db0a21e41..3f2f15ba680 100644
--- a/docs/userguide/testing.rst
+++ b/docs/userguide/testing.rst
@@ -197,6 +197,20 @@ Example:
def test_other(celery_worker):
...
+Heartbeats are disabled by default which means that the test worker doesn't
+send events for ``worker-online``, ``worker-offline`` and ``worker-heartbeat``.
+To enable heartbeats modify the :func:`celery_worker_parameters` fixture:
+
+.. code-block:: python
+
+ # Put this in your conftest.py
+ @pytest.fixture(scope="session")
+ def celery_worker_parameters():
+ return {"without_heartbeat": False}
+ ...
+
+
+
Session scope
^^^^^^^^^^^^^
From f1d387b1fa1575f9eb17fa5ce5e17234c2d9a70c Mon Sep 17 00:00:00 2001
From: Gabriel Augendre
Date: Tue, 17 Nov 2020 09:41:00 +0100
Subject: [PATCH 154/415] Add a classifier indicating support for python 3.9
Since https://github.com/celery/celery/pull/6418, the tests seem to pass on Python 3.9.
Let's make this official!
---
setup.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/setup.py b/setup.py
index 35f2dd6b084..d4e27c1226e 100644
--- a/setup.py
+++ b/setup.py
@@ -192,6 +192,7 @@ def run_tests(self):
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Operating System :: OS Independent"
From 7f48a6dd2a79dffa247892ef8c593ff7d24b22d3 Mon Sep 17 00:00:00 2001
From: AbdealiJK
Date: Wed, 24 Feb 2021 19:57:42 +0530
Subject: [PATCH 155/415] test_canvas: Add test for chain-in-chain (#6201)
Add test case for the issue where a chain in a chain does not
work when using .apply(). This works fine with .apply_async().
The inner chain should have only 1 item.
---
t/integration/test_canvas.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index fe594807ee5..2c96aa95b44 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -1158,6 +1158,25 @@ def test_chord_in_chain_with_args(self, manager):
res1 = c1.apply(args=(1,))
assert res1.get(timeout=TIMEOUT) == [1, 1]
+ @pytest.mark.xfail(reason="Issue #6200")
+ def test_chain_in_chain_with_args(self):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ c1 = chain( # NOTE: This chain should have only 1 chain inside it
+ chain(
+ identity.s(),
+ identity.s(),
+ ),
+ )
+
+ res1 = c1.apply_async(args=(1,))
+ assert res1.get(timeout=TIMEOUT) == 1
+ res1 = c1.apply(args=(1,))
+ assert res1.get(timeout=TIMEOUT) == 1
+
@flaky
def test_large_header(self, manager):
try:
From c86930dc5bfd4d2542724888dbf1eef5c96aaa5d Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 24 Feb 2021 19:50:34 +0200
Subject: [PATCH 156/415] Configure the open collective bot.
---
.github/opencollective.yml | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
create mode 100644 .github/opencollective.yml
diff --git a/.github/opencollective.yml b/.github/opencollective.yml
new file mode 100644
index 00000000000..be703c8b871
--- /dev/null
+++ b/.github/opencollective.yml
@@ -0,0 +1,18 @@
+collective: celery
+tiers:
+ - tiers: '*'
+ labels: ['Backer ❤️']
+ message: 'Hey . Thank you for supporting the project!:heart:'
+ - tiers: ['Basic Sponsor', 'Sponsor', 'Silver Sponsor', 'Gold Sponsor']
+ labels: ['Sponsor ❤️']
+ message: |
+ Thank you for sponsoring the project!:heart::heart::heart:
+ Resolving this issue is one of our top priorities.
+ One of @celery/core-developers will triage it shortly.
+invitation: |
+ Hey :wave:,
+ Thank you for opening an issue. We will get back to you as soon as we can.
+ Also, check out our [Open Collective]() and consider backing us - every little helps!
+
+ We also offer priority support for our sponsors.
+ If you require immediate assistance please consider sponsoring us.
From 34c469758f39a4d2ba693d63952df95525666826 Mon Sep 17 00:00:00 2001
From: Anatoliy
Date: Thu, 25 Feb 2021 16:49:59 +0300
Subject: [PATCH 157/415] New celery beat run command for supervisor (#6645)
* Update extra/supervisord/celeryd.conf line 18
Adding compatibility with celery 5.0.6 which have different worker 'run' command
* Changes celery beat run command in supervisor's celerybeat.conf file
Due to new celery 5.0.5 syntax
---
extra/supervisord/celerybeat.conf | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/extra/supervisord/celerybeat.conf b/extra/supervisord/celerybeat.conf
index c920b30dfda..8710c31ac1f 100644
--- a/extra/supervisord/celerybeat.conf
+++ b/extra/supervisord/celerybeat.conf
@@ -4,7 +4,7 @@
[program:celerybeat]
; Set full path to celery program if using virtualenv
-command=celery beat -A myapp --schedule /var/lib/celery/beat.db --loglevel=INFO
+command=celery -A myapp beat --schedule /var/lib/celery/beat.db --loglevel=INFO
; remove the -A myapp argument if you aren't using an app instance
From 50ae4331cec1e2d61f536b406b0ebfefe7f1a495 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 28 Feb 2021 11:14:40 +0200
Subject: [PATCH 158/415] Revert "Revert "redis: Support Sentinel with SSL"
(#6518)" (#6647)
This reverts commit 0fa4db8889325fd774f7e89ebb219a87fc1d8cfb.
---
celery/backends/redis.py | 20 ++++++++++++++++++--
t/unit/backends/test_redis.py | 31 +++++++++++++++++++++++++++++++
2 files changed, 49 insertions(+), 2 deletions(-)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index 3ffdf70aa3d..fca058ee584 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -186,6 +186,7 @@ class RedisBackend(BaseKeyValueStoreBackend, AsyncBackendMixin):
#: :pypi:`redis` client module.
redis = redis
+ connection_class_ssl = redis.SSLConnection if redis else None
#: Maximum number of connections in the pool.
max_connections = None
@@ -241,7 +242,7 @@ def __init__(self, host=None, port=None, db=None, password=None,
ssl = _get('redis_backend_use_ssl')
if ssl:
self.connparams.update(ssl)
- self.connparams['connection_class'] = redis.SSLConnection
+ self.connparams['connection_class'] = self.connection_class_ssl
if url:
self.connparams = self._params_from_url(url, self.connparams)
@@ -250,7 +251,7 @@ def __init__(self, host=None, port=None, db=None, password=None,
# redis_backend_use_ssl dict, check ssl_cert_reqs is valid. If set
# via query string ssl_cert_reqs will be a string so convert it here
if ('connection_class' in self.connparams and
- self.connparams['connection_class'] is redis.SSLConnection):
+ issubclass(self.connparams['connection_class'], redis.SSLConnection)):
ssl_cert_reqs_missing = 'MISSING'
ssl_string_to_constant = {'CERT_REQUIRED': CERT_REQUIRED,
'CERT_OPTIONAL': CERT_OPTIONAL,
@@ -546,10 +547,25 @@ def __reduce__(self, args=(), kwargs=None):
)
+if getattr(redis, "sentinel", None):
+ class SentinelManagedSSLConnection(
+ redis.sentinel.SentinelManagedConnection,
+ redis.SSLConnection):
+ """Connect to a Redis server using Sentinel + TLS.
+
+ Use Sentinel to identify which Redis server is the current master
+ to connect to and when connecting to the Master server, use an
+ SSL Connection.
+ """
+
+ pass
+
+
class SentinelBackend(RedisBackend):
"""Redis sentinel task result store."""
sentinel = getattr(redis, "sentinel", None)
+ connection_class_ssl = SentinelManagedSSLConnection if sentinel else None
def __init__(self, *args, **kwargs):
if self.sentinel is None:
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index 75d917b5cef..fb236426f06 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -1146,3 +1146,34 @@ def test_get_pool(self):
)
pool = x._get_pool(**x.connparams)
assert pool
+
+ def test_backend_ssl(self):
+ pytest.importorskip('redis')
+
+ from celery.backends.redis import SentinelBackend
+ self.app.conf.redis_backend_use_ssl = {
+ 'ssl_cert_reqs': "CERT_REQUIRED",
+ 'ssl_ca_certs': '/path/to/ca.crt',
+ 'ssl_certfile': '/path/to/client.crt',
+ 'ssl_keyfile': '/path/to/client.key',
+ }
+ self.app.conf.redis_socket_timeout = 30.0
+ self.app.conf.redis_socket_connect_timeout = 100.0
+ x = SentinelBackend(
+ 'sentinel://:bosco@vandelay.com:123//1', app=self.app,
+ )
+ assert x.connparams
+ assert len(x.connparams['hosts']) == 1
+ assert x.connparams['hosts'][0]['host'] == 'vandelay.com'
+ assert x.connparams['hosts'][0]['db'] == 1
+ assert x.connparams['hosts'][0]['port'] == 123
+ assert x.connparams['hosts'][0]['password'] == 'bosco'
+ assert x.connparams['socket_timeout'] == 30.0
+ assert x.connparams['socket_connect_timeout'] == 100.0
+ assert x.connparams['ssl_cert_reqs'] == ssl.CERT_REQUIRED
+ assert x.connparams['ssl_ca_certs'] == '/path/to/ca.crt'
+ assert x.connparams['ssl_certfile'] == '/path/to/client.crt'
+ assert x.connparams['ssl_keyfile'] == '/path/to/client.key'
+
+ from celery.backends.redis import SentinelManagedSSLConnection
+ assert x.connparams['connection_class'] is SentinelManagedSSLConnection
From 5baf972fb6e74f5e831c0bc435f3e02965563779 Mon Sep 17 00:00:00 2001
From: Noam
Date: Mon, 1 Mar 2021 15:06:00 +0200
Subject: [PATCH 159/415] Fixed default visibility timeout note in sqs
documentation.
---
docs/getting-started/backends-and-brokers/sqs.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/getting-started/backends-and-brokers/sqs.rst b/docs/getting-started/backends-and-brokers/sqs.rst
index 2e41ce4ef9e..74d2f149f54 100644
--- a/docs/getting-started/backends-and-brokers/sqs.rst
+++ b/docs/getting-started/backends-and-brokers/sqs.rst
@@ -82,7 +82,7 @@ This option is set via the :setting:`broker_transport_options` setting::
broker_transport_options = {'visibility_timeout': 3600} # 1 hour.
-The default visibility timeout is 30 seconds.
+The default visibility timeout is 30 minutes.
Polling Interval
----------------
From ade944e218e3fd455e96b8d62766f1ed70143d97 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Vincent=20Sch=C3=A4nzer?=
Date: Tue, 2 Mar 2021 09:16:48 +0100
Subject: [PATCH 160/415] Add note for max_retries default in documentation
The documentation lacks the mention of the default value for Task.max_retries. Since the last sentence talks about the behavior with a None value, it can be mistakenly assumed that this is the default value.
---
docs/userguide/tasks.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/userguide/tasks.rst b/docs/userguide/tasks.rst
index 935f15f92c2..e41da045ea7 100644
--- a/docs/userguide/tasks.rst
+++ b/docs/userguide/tasks.rst
@@ -825,7 +825,7 @@ You can also set `autoretry_for`, `max_retries`, `retry_backoff`, `retry_backoff
.. attribute:: Task.max_retries
A number. Maximum number of retries before giving up. A value of ``None``
- means task will retry forever.
+ means task will retry forever. By default, this option is set to ``3``.
.. attribute:: Task.retry_backoff
From 7451f60291c6ec4f78129b4ed181d42a7baee1a6 Mon Sep 17 00:00:00 2001
From: Swen Kooij
Date: Sat, 27 Feb 2021 08:35:59 +0200
Subject: [PATCH 161/415] Simulate more exhaustive delivery info in apply()
When calling tasks directly, all options such as priority, exchange
and routing_key are accepted, but ignored.
All though these options take no effect when calling tasks
eagerly, it is useful to keep the data around. An application
might take action based on the priority, exchange or the
routing key.
It is especially useful when writing unit tests. This allows
tasks to be run eagerly, yet test that the application behaves
correctly when these options are passed.
---
celery/app/task.py | 7 ++++++-
t/unit/tasks/test_tasks.py | 20 ++++++++++++++++++++
2 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/celery/app/task.py b/celery/app/task.py
index 5634c442152..1d4c974cb22 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -764,7 +764,12 @@ def apply(self, args=None, kwargs=None,
'callbacks': maybe_list(link),
'errbacks': maybe_list(link_error),
'headers': headers,
- 'delivery_info': {'is_eager': True},
+ 'delivery_info': {
+ 'is_eager': True,
+ 'exchange': options.get('exchange'),
+ 'routing_key': options.get('routing_key'),
+ 'priority': options.get('priority'),
+ },
}
tb = None
tracer = build_tracer(
diff --git a/t/unit/tasks/test_tasks.py b/t/unit/tasks/test_tasks.py
index 8e1c05a5796..900d7decdbc 100644
--- a/t/unit/tasks/test_tasks.py
+++ b/t/unit/tasks/test_tasks.py
@@ -1282,6 +1282,26 @@ def test_apply(self):
with pytest.raises(KeyError):
f.get()
+ def test_apply_simulates_delivery_info(self):
+ self.task_check_request_context.request_stack.push = Mock()
+
+ self.task_check_request_context.apply(
+ priority=4,
+ routing_key='myroutingkey',
+ exchange='myexchange',
+ )
+
+ self.task_check_request_context.request_stack.push.assert_called_once()
+
+ request = self.task_check_request_context.request_stack.push.call_args[0][0]
+
+ assert request.delivery_info == {
+ 'is_eager': True,
+ 'exchange': 'myexchange',
+ 'routing_key': 'myroutingkey',
+ 'priority': 4,
+ }
+
class test_apply_async(TasksCase):
def common_send_task_arguments(self):
From e913312291ff9403b0cd32448cb930829c62cf15 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 2 Mar 2021 10:42:38 +0200
Subject: [PATCH 162/415] Only run tests when any Python file has changed.
(#6650)
---
.github/workflows/python-package.yml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index f6558ba8334..af1ffb27f4f 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -6,8 +6,12 @@ name: Celery
on:
push:
branches: [ master ]
+ paths:
+ - '**.py'
pull_request:
branches: [ master ]
+ paths:
+ - '**.py'
jobs:
build:
From 0c4fa09c6215b5fbb2ad609758926f2cd4e7db82 Mon Sep 17 00:00:00 2001
From: gal cohen
Date: Tue, 2 Mar 2021 12:24:59 +0200
Subject: [PATCH 163/415] SQS broker back-off policy - documentation only
(#6648)
* celery sqs retry policy
* add proper doc
* spacing
* docs
* rename policy
* improve docstring
* add doc
* docs update
Co-authored-by: galcohen
---
.../backends-and-brokers/sqs.rst | 40 +++++++++++++++++++
1 file changed, 40 insertions(+)
diff --git a/docs/getting-started/backends-and-brokers/sqs.rst b/docs/getting-started/backends-and-brokers/sqs.rst
index 74d2f149f54..90be6a998b8 100644
--- a/docs/getting-started/backends-and-brokers/sqs.rst
+++ b/docs/getting-started/backends-and-brokers/sqs.rst
@@ -150,6 +150,46 @@ setting::
}
}
+Back-off policy
+------------------------
+Back-off policy is using SQS visibility timeout mechanism altering the time difference between task retries.
+The number of retries is managed by SQS (specifically by the ``ApproximateReceiveCount`` message attribute) and no further action is required by the user.
+
+Configuring the queues and backoff policy::
+
+ broker_transport_options = {
+ 'predefined_queues': {
+ 'my-q': {
+ 'url': 'https://ap-southeast-2.queue.amazonaws.com/123456/my-q',
+ 'access_key_id': 'xxx',
+ 'secret_access_key': 'xxx',
+ 'backoff_policy': {1: 10, 2: 20, 3: 40, 4: 80, 5: 320, 6: 640},
+ 'backoff_tasks': ['svc.tasks.tasks.task1']
+ }
+ }
+ }
+
+
+``backoff_policy`` dictionary where key is number of retries, and value is delay seconds between retries (i.e
+SQS visibility timeout)
+``backoff_tasks`` list of task names to apply the above policy
+
+The above policy:
+
++-----------------------------------------+--------------------------------------------+
+| **Attempt** | **Delay** |
++-----------------------------------------+--------------------------------------------+
+| ``2nd attempt`` | 20 seconds |
++-----------------------------------------+--------------------------------------------+
+| ``3rd attempt`` | 40 seconds |
++-----------------------------------------+--------------------------------------------+
+| ``4th attempt`` | 80 seconds |
++-----------------------------------------+--------------------------------------------+
+| ``5th attempt`` | 320 seconds |
++-----------------------------------------+--------------------------------------------+
+| ``6th attempt`` | 640 seconds |
++-----------------------------------------+--------------------------------------------+
+
.. _sqs-caveats:
From cfa1b418cca81af069599a9a56c340e8a27ff2fc Mon Sep 17 00:00:00 2001
From: galcohen
Date: Tue, 2 Mar 2021 15:39:57 +0200
Subject: [PATCH 164/415] improve docs
---
docs/getting-started/backends-and-brokers/sqs.rst | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/getting-started/backends-and-brokers/sqs.rst b/docs/getting-started/backends-and-brokers/sqs.rst
index 90be6a998b8..47ec6d8f864 100644
--- a/docs/getting-started/backends-and-brokers/sqs.rst
+++ b/docs/getting-started/backends-and-brokers/sqs.rst
@@ -153,6 +153,7 @@ setting::
Back-off policy
------------------------
Back-off policy is using SQS visibility timeout mechanism altering the time difference between task retries.
+The mechanism changes message specific ``visibility timeout`` from queue ``Default visibility timeout`` to policy configured timeout.
The number of retries is managed by SQS (specifically by the ``ApproximateReceiveCount`` message attribute) and no further action is required by the user.
Configuring the queues and backoff policy::
From 2b7a8fa943f6cd7c044d69ab194bbdc12f8bc745 Mon Sep 17 00:00:00 2001
From: Guillaume DE SUSANNE D'EPINAY
Date: Wed, 3 Mar 2021 18:26:36 +0100
Subject: [PATCH 165/415] fix: configuration variables names
---
docs/userguide/configuration.rst | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index 6e9c600b5f2..cf85255ec54 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -146,8 +146,9 @@ have been moved into a new ``task_`` prefix.
``CELERY_SEND_SENT_EVENT`` :setting:`task_send_sent_event`
``CELERY_SERIALIZER`` :setting:`task_serializer`
``CELERYD_SOFT_TIME_LIMIT`` :setting:`task_soft_time_limit`
+``CELERY_TASK_TRACK_STARTED`` :setting:`task_track_started`
+``CELERY_TASK_REJECT_ON_WORKER_LOST`` :setting:`task_reject_on_worker_lost`
``CELERYD_TIME_LIMIT`` :setting:`task_time_limit`
-``CELERY_TRACK_STARTED`` :setting:`task_track_started`
``CELERYD_AGENT`` :setting:`worker_agent`
``CELERYD_AUTOSCALER`` :setting:`worker_autoscaler`
``CELERYD_CONCURRENCY`` :setting:`worker_concurrency`
From 6734024e59a60ef2814eabed63365829a3270cc2 Mon Sep 17 00:00:00 2001
From: Matt Hoffman
Date: Thu, 4 Mar 2021 08:22:31 -0500
Subject: [PATCH 166/415] start chord header tasks as soon as possible (#6576)
* start chord header tasks as soon as possible
Fixes #3012. Previously, subtasks were not submitted to the broker for
execution until the entire generator was consumed.
* Update celery/canvas.py as per review
Co-authored-by: Omer Katz
* keeps chord_length terminology consistent
* refactors unit test
* Update celery/canvas.py
Co-authored-by: Omer Katz
* Update celery/canvas.py
Co-authored-by: Omer Katz
* Fix formatting.
* adds comments and fixes test
Co-authored-by: Asif Saif Uddin
Co-authored-by: Omer Katz
---
celery/backends/base.py | 9 +-
celery/backends/cache.py | 6 +-
celery/backends/redis.py | 145 +++++++++++++++++---------------
celery/canvas.py | 119 +++++++++++++++-----------
celery/utils/functional.py | 15 +++-
t/integration/tasks.py | 8 ++
t/integration/test_canvas.py | 20 ++++-
t/unit/backends/test_base.py | 18 ++--
t/unit/backends/test_cache.py | 12 +--
t/unit/backends/test_redis.py | 98 ++++++++++-----------
t/unit/tasks/test_canvas.py | 11 +++
t/unit/tasks/test_chord.py | 10 +++
t/unit/utils/test_functional.py | 12 +--
13 files changed, 292 insertions(+), 191 deletions(-)
diff --git a/celery/backends/base.py b/celery/backends/base.py
index b18f40887e2..7c5dcfa357c 100644
--- a/celery/backends/base.py
+++ b/celery/backends/base.py
@@ -590,6 +590,9 @@ def add_to_chord(self, chord_id, result):
def on_chord_part_return(self, request, state, result, **kwargs):
pass
+ def set_chord_size(self, group_id, chord_size):
+ pass
+
def fallback_chord_unlock(self, header_result, body, countdown=1,
**kwargs):
kwargs['result'] = [r.as_tuple() for r in header_result]
@@ -605,8 +608,9 @@ def fallback_chord_unlock(self, header_result, body, countdown=1,
def ensure_chords_allowed(self):
pass
- def apply_chord(self, header_result, body, **kwargs):
+ def apply_chord(self, header_result_args, body, **kwargs):
self.ensure_chords_allowed()
+ header_result = self.app.GroupResult(*header_result_args)
self.fallback_chord_unlock(header_result, body, **kwargs)
def current_task_children(self, request=None):
@@ -887,8 +891,9 @@ def _restore_group(self, group_id):
meta['result'] = result_from_tuple(result, self.app)
return meta
- def _apply_chord_incr(self, header_result, body, **kwargs):
+ def _apply_chord_incr(self, header_result_args, body, **kwargs):
self.ensure_chords_allowed()
+ header_result = self.app.GroupResult(*header_result_args)
header_result.save(backend=self)
def on_chord_part_return(self, request, state, result, **kwargs):
diff --git a/celery/backends/cache.py b/celery/backends/cache.py
index e340f31b7f6..f3d13d95304 100644
--- a/celery/backends/cache.py
+++ b/celery/backends/cache.py
@@ -128,11 +128,11 @@ def set(self, key, value):
def delete(self, key):
return self.client.delete(key)
- def _apply_chord_incr(self, header_result, body, **kwargs):
- chord_key = self.get_key_for_chord(header_result.id)
+ def _apply_chord_incr(self, header_result_args, body, **kwargs):
+ chord_key = self.get_key_for_chord(header_result_args[0])
self.client.set(chord_key, 0, time=self.expires)
return super()._apply_chord_incr(
- header_result, body, **kwargs)
+ header_result_args, body, **kwargs)
def incr(self, key):
return self.client.incr(key)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index fca058ee584..d684c196fd7 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -15,7 +15,7 @@
from celery.exceptions import (BackendStoreError, ChordError,
ImproperlyConfigured)
from celery.result import GroupResult, allow_join_result
-from celery.utils.functional import dictfilter
+from celery.utils.functional import _regen, dictfilter
from celery.utils.log import get_logger
from celery.utils.time import humanize_seconds
@@ -370,7 +370,7 @@ def on_connection_error(self, max_retries, exc, intervals, retries):
return tts
def set(self, key, value, **retry_policy):
- if len(value) > self._MAX_STR_VALUE_SIZE:
+ if isinstance(value, str) and len(value) > self._MAX_STR_VALUE_SIZE:
raise BackendStoreError('value too large for Redis backend')
return self.ensure(self._set, (key, value), **retry_policy)
@@ -410,15 +410,20 @@ def _unpack_chord_result(self, tup, decode,
raise ChordError(f'Dependency {tid} raised {retval!r}')
return retval
- def apply_chord(self, header_result, body, **kwargs):
+ def set_chord_size(self, group_id, chord_size):
+ self.set(self.get_key_for_group(group_id, '.s'), chord_size)
+
+ def apply_chord(self, header_result_args, body, **kwargs):
# If any of the child results of this chord are complex (ie. group
# results themselves), we need to save `header_result` to ensure that
# the expected structure is retained when we finish the chord and pass
# the results onward to the body in `on_chord_part_return()`. We don't
# do this is all cases to retain an optimisation in the common case
# where a chord header is comprised of simple result objects.
- if any(isinstance(nr, GroupResult) for nr in header_result.results):
- header_result.save(backend=self)
+ if not isinstance(header_result_args[1], _regen):
+ header_result = self.app.GroupResult(*header_result_args)
+ if any(isinstance(nr, GroupResult) for nr in header_result.results):
+ header_result.save(backend=self)
@cached_property
def _chord_zset(self):
@@ -440,6 +445,7 @@ def on_chord_part_return(self, request, state, result,
client = self.client
jkey = self.get_key_for_group(gid, '.j')
tkey = self.get_key_for_group(gid, '.t')
+ skey = self.get_key_for_group(gid, '.s')
result = self.encode_result(result, state)
encoded = self.encode([1, tid, state, result])
with client.pipeline() as pipe:
@@ -447,77 +453,80 @@ def on_chord_part_return(self, request, state, result,
pipe.zadd(jkey, {encoded: group_index}).zcount(jkey, "-inf", "+inf")
if self._chord_zset
else pipe.rpush(jkey, encoded).llen(jkey)
- ).get(tkey)
+ ).get(tkey).get(skey)
if self.expires:
pipeline = pipeline \
.expire(jkey, self.expires) \
- .expire(tkey, self.expires)
+ .expire(tkey, self.expires) \
+ .expire(skey, self.expires)
- _, readycount, totaldiff = pipeline.execute()[:3]
+ _, readycount, totaldiff, chord_size_bytes = pipeline.execute()[:4]
totaldiff = int(totaldiff or 0)
- try:
- callback = maybe_signature(request.chord, app=app)
- total = callback['chord_size'] + totaldiff
- if readycount == total:
- header_result = GroupResult.restore(gid)
- if header_result is not None:
- # If we manage to restore a `GroupResult`, then it must
- # have been complex and saved by `apply_chord()` earlier.
- #
- # Before we can join the `GroupResult`, it needs to be
- # manually marked as ready to avoid blocking
- header_result.on_ready()
- # We'll `join()` it to get the results and ensure they are
- # structured as intended rather than the flattened version
- # we'd construct without any other information.
- join_func = (
- header_result.join_native
- if header_result.supports_native_join
- else header_result.join
- )
- with allow_join_result():
- resl = join_func(
- timeout=app.conf.result_chord_join_timeout,
- propagate=True
+ if chord_size_bytes:
+ try:
+ callback = maybe_signature(request.chord, app=app)
+ total = int(chord_size_bytes) + totaldiff
+ if readycount == total:
+ header_result = GroupResult.restore(gid)
+ if header_result is not None:
+ # If we manage to restore a `GroupResult`, then it must
+ # have been complex and saved by `apply_chord()` earlier.
+ #
+ # Before we can join the `GroupResult`, it needs to be
+ # manually marked as ready to avoid blocking
+ header_result.on_ready()
+ # We'll `join()` it to get the results and ensure they are
+ # structured as intended rather than the flattened version
+ # we'd construct without any other information.
+ join_func = (
+ header_result.join_native
+ if header_result.supports_native_join
+ else header_result.join
)
- else:
- # Otherwise simply extract and decode the results we
- # stashed along the way, which should be faster for large
- # numbers of simple results in the chord header.
- decode, unpack = self.decode, self._unpack_chord_result
- with client.pipeline() as pipe:
- if self._chord_zset:
- pipeline = pipe.zrange(jkey, 0, -1)
- else:
- pipeline = pipe.lrange(jkey, 0, total)
- resl, = pipeline.execute()
- resl = [unpack(tup, decode) for tup in resl]
- try:
- callback.delay(resl)
- except Exception as exc: # pylint: disable=broad-except
- logger.exception(
- 'Chord callback for %r raised: %r', request.group, exc)
- return self.chord_error_from_stack(
- callback,
- ChordError(f'Callback error: {exc!r}'),
- )
- finally:
- with client.pipeline() as pipe:
- _, _ = pipe \
- .delete(jkey) \
- .delete(tkey) \
- .execute()
- except ChordError as exc:
- logger.exception('Chord %r raised: %r', request.group, exc)
- return self.chord_error_from_stack(callback, exc)
- except Exception as exc: # pylint: disable=broad-except
- logger.exception('Chord %r raised: %r', request.group, exc)
- return self.chord_error_from_stack(
- callback,
- ChordError(f'Join error: {exc!r}'),
- )
+ with allow_join_result():
+ resl = join_func(
+ timeout=app.conf.result_chord_join_timeout,
+ propagate=True
+ )
+ else:
+ # Otherwise simply extract and decode the results we
+ # stashed along the way, which should be faster for large
+ # numbers of simple results in the chord header.
+ decode, unpack = self.decode, self._unpack_chord_result
+ with client.pipeline() as pipe:
+ if self._chord_zset:
+ pipeline = pipe.zrange(jkey, 0, -1)
+ else:
+ pipeline = pipe.lrange(jkey, 0, total)
+ resl, = pipeline.execute()
+ resl = [unpack(tup, decode) for tup in resl]
+ try:
+ callback.delay(resl)
+ except Exception as exc: # pylint: disable=broad-except
+ logger.exception(
+ 'Chord callback for %r raised: %r', request.group, exc)
+ return self.chord_error_from_stack(
+ callback,
+ ChordError(f'Callback error: {exc!r}'),
+ )
+ finally:
+ with client.pipeline() as pipe:
+ pipe \
+ .delete(jkey) \
+ .delete(tkey) \
+ .delete(skey) \
+ .execute()
+ except ChordError as exc:
+ logger.exception('Chord %r raised: %r', request.group, exc)
+ return self.chord_error_from_stack(callback, exc)
+ except Exception as exc: # pylint: disable=broad-except
+ logger.exception('Chord %r raised: %r', request.group, exc)
+ return self.chord_error_from_stack(
+ callback,
+ ChordError(f'Join error: {exc!r}'),
+ )
def _create_client(self, **params):
return self._get_client()(
diff --git a/celery/canvas.py b/celery/canvas.py
index a4de76428dc..47afd2be9bc 100644
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -13,6 +13,7 @@
from functools import partial as _partial
from functools import reduce
from operator import itemgetter
+from types import GeneratorType
from kombu.utils.functional import fxrange, reprcall
from kombu.utils.objects import cached_property
@@ -25,7 +26,7 @@
from celery.utils.collections import ChainMap
from celery.utils.functional import _regen
from celery.utils.functional import chunks as _chunks
-from celery.utils.functional import (is_list, maybe_list, regen,
+from celery.utils.functional import (is_list, lookahead, maybe_list, regen,
seq_concat_item, seq_concat_seq)
from celery.utils.objects import getitem_property
from celery.utils.text import remove_repeating_from_task, truncate
@@ -56,12 +57,6 @@ def task_name_from(task):
return getattr(task, 'name', task)
-def _upgrade(fields, sig):
- """Used by custom signatures in .from_dict, to keep common fields."""
- sig.update(chord_size=fields.get('chord_size'))
- return sig
-
-
@abstract.CallableSignature.register
class Signature(dict):
"""Task Signature.
@@ -165,7 +160,6 @@ def __init__(self, task=None, args=None, kwargs=None, options=None,
options=dict(options or {}, **ex),
subtask_type=subtask_type,
immutable=immutable,
- chord_size=None,
)
def __call__(self, *partial_args, **partial_kwargs):
@@ -265,7 +259,6 @@ def clone(self, args=None, kwargs=None, **opts):
'kwargs': kwargs,
'options': deepcopy(opts),
'subtask_type': self.subtask_type,
- 'chord_size': self.chord_size,
'immutable': self.immutable},
app=self._app)
signature._type = self._type
@@ -530,8 +523,6 @@ def _apply_async(self):
kwargs = getitem_property('kwargs', 'Keyword arguments to task.')
options = getitem_property('options', 'Task execution options.')
subtask_type = getitem_property('subtask_type', 'Type of signature')
- chord_size = getitem_property(
- 'chord_size', 'Size of chord (if applicable)')
immutable = getitem_property(
'immutable', 'Flag set if no longer accepts new arguments')
@@ -604,7 +595,7 @@ def from_dict(cls, d, app=None):
if isinstance(tasks, tuple): # aaaargh
tasks = d['kwargs']['tasks'] = list(tasks)
tasks = [maybe_signature(task, app=app) for task in tasks]
- return _upgrade(d, _chain(tasks, app=app, **d['options']))
+ return _chain(tasks, app=app, **d['options'])
def __init__(self, *tasks, **options):
tasks = (regen(tasks[0]) if len(tasks) == 1 and is_list(tasks[0])
@@ -908,9 +899,7 @@ class _basemap(Signature):
@classmethod
def from_dict(cls, d, app=None):
- return _upgrade(
- d, cls(*cls._unpack_args(d['kwargs']), app=app, **d['options']),
- )
+ return cls(*cls._unpack_args(d['kwargs']), app=app, **d['options'])
def __init__(self, task, it, **options):
Signature.__init__(
@@ -964,10 +953,7 @@ class chunks(Signature):
@classmethod
def from_dict(cls, d, app=None):
- return _upgrade(
- d, chunks(*cls._unpack_args(
- d['kwargs']), app=app, **d['options']),
- )
+ return chunks(*cls._unpack_args(d['kwargs']), app=app, **d['options'])
def __init__(self, task, it, n, **options):
Signature.__init__(
@@ -1008,7 +994,10 @@ def _maybe_group(tasks, app):
elif isinstance(tasks, abstract.CallableSignature):
tasks = [tasks]
else:
- tasks = [signature(t, app=app) for t in tasks]
+ if isinstance(tasks, GeneratorType):
+ tasks = regen(signature(t, app=app) for t in tasks)
+ else:
+ tasks = [signature(t, app=app) for t in tasks]
return tasks
@@ -1055,9 +1044,7 @@ def from_dict(cls, d, app=None):
d["kwargs"]["tasks"] = rebuilt_tasks = type(orig_tasks)((
maybe_signature(task, app=app) for task in orig_tasks
))
- return _upgrade(
- d, group(rebuilt_tasks, app=app, **d['options']),
- )
+ return group(rebuilt_tasks, app=app, **d['options'])
def __init__(self, *tasks, **options):
if len(tasks) == 1:
@@ -1127,7 +1114,7 @@ def apply(self, args=None, kwargs=None, **options):
options, group_id, root_id = self._freeze_gid(options)
tasks = self._prepared(self.tasks, [], group_id, root_id, app)
return app.GroupResult(group_id, [
- sig.apply(args=args, kwargs=kwargs, **options) for sig, _ in tasks
+ sig.apply(args=args, kwargs=kwargs, **options) for sig, _, _ in tasks
])
def set_immutable(self, immutable):
@@ -1170,7 +1157,7 @@ def _prepared(self, tasks, partial_args, group_id, root_id, app,
else:
if partial_args and not task.immutable:
task.args = tuple(partial_args) + tuple(task.args)
- yield task, task.freeze(group_id=group_id, root_id=root_id)
+ yield task, task.freeze(group_id=group_id, root_id=root_id), group_id
def _apply_tasks(self, tasks, producer=None, app=None, p=None,
add_to_parent=None, chord=None,
@@ -1179,12 +1166,26 @@ def _apply_tasks(self, tasks, producer=None, app=None, p=None,
# XXX chord is also a class in outer scope.
app = app or self.app
with app.producer_or_acquire(producer) as producer:
- for sig, res in tasks:
+ # Iterate through tasks two at a time. If tasks is a generator,
+ # we are able to tell when we are at the end by checking if
+ # next_task is None. This enables us to set the chord size
+ # without burning through the entire generator. See #3021.
+ for task_index, (current_task, next_task) in enumerate(
+ lookahead(tasks)
+ ):
+ sig, res, group_id = current_task
+ _chord = sig.options.get("chord") or chord
+ if _chord is not None and next_task is None:
+ chord_size = task_index + 1
+ if isinstance(sig, _chain):
+ if sig.tasks[-1].subtask_type == 'chord':
+ chord_size = sig.tasks[-1].__length_hint__()
+ else:
+ chord_size = task_index + len(sig.tasks[-1])
+ app.backend.set_chord_size(group_id, chord_size)
sig.apply_async(producer=producer, add_to_parent=False,
- chord=sig.options.get('chord') or chord,
- args=args, kwargs=kwargs,
+ chord=_chord, args=args, kwargs=kwargs,
**options)
-
# adding callback to result, such that it will gradually
# fulfill the barrier.
#
@@ -1204,10 +1205,10 @@ def _freeze_gid(self, options):
options.pop('task_id', uuid()))
return options, group_id, options.get('root_id')
- def freeze(self, _id=None, group_id=None, chord=None,
- root_id=None, parent_id=None, group_index=None):
+ def _freeze_group_tasks(self, _id=None, group_id=None, chord=None,
+ root_id=None, parent_id=None, group_index=None):
# pylint: disable=redefined-outer-name
- # XXX chord is also a class in outer scope.
+ # XXX chord is also a class in outer scope.
opts = self.options
try:
gid = opts['task_id']
@@ -1221,20 +1222,42 @@ def freeze(self, _id=None, group_id=None, chord=None,
opts['group_index'] = group_index
root_id = opts.setdefault('root_id', root_id)
parent_id = opts.setdefault('parent_id', parent_id)
- new_tasks = []
- # Need to unroll subgroups early so that chord gets the
- # right result instance for chord_unlock etc.
- results = list(self._freeze_unroll(
- new_tasks, group_id, chord, root_id, parent_id,
- ))
- if isinstance(self.tasks, MutableSequence):
- self.tasks[:] = new_tasks
+ if isinstance(self.tasks, _regen):
+ # We are draining from a geneator here.
+ tasks1, tasks2 = itertools.tee(self._unroll_tasks(self.tasks))
+ results = regen(self._freeze_tasks(tasks1, group_id, chord, root_id, parent_id))
+ self.tasks = regen(x[0] for x in zip(tasks2, results))
else:
- self.tasks = new_tasks
- return self.app.GroupResult(gid, results)
+ new_tasks = []
+ # Need to unroll subgroups early so that chord gets the
+ # right result instance for chord_unlock etc.
+ results = list(self._freeze_unroll(
+ new_tasks, group_id, chord, root_id, parent_id,
+ ))
+ if isinstance(self.tasks, MutableSequence):
+ self.tasks[:] = new_tasks
+ else:
+ self.tasks = new_tasks
+ return gid, results
+
+ def freeze(self, _id=None, group_id=None, chord=None,
+ root_id=None, parent_id=None, group_index=None):
+ return self.app.GroupResult(*self._freeze_group_tasks(_id=_id, group_id=group_id,
+ chord=chord, root_id=root_id, parent_id=parent_id, group_index=group_index))
_freeze = freeze
+ def _freeze_tasks(self, tasks, group_id, chord, root_id, parent_id):
+ yield from (task.freeze(group_id=group_id,
+ chord=chord,
+ root_id=root_id,
+ parent_id=parent_id,
+ group_index=group_index)
+ for group_index, task in enumerate(tasks))
+
+ def _unroll_tasks(self, tasks):
+ yield from (maybe_signature(task, app=self._app).clone() for task in tasks)
+
def _freeze_unroll(self, new_tasks, group_id, chord, root_id, parent_id):
# pylint: disable=redefined-outer-name
# XXX chord is also a class in outer scope.
@@ -1305,7 +1328,7 @@ class chord(Signature):
def from_dict(cls, d, app=None):
options = d.copy()
args, options['kwargs'] = cls._unpack_args(**options['kwargs'])
- return _upgrade(d, cls(*args, app=app, **options))
+ return cls(*args, app=app, **options)
@staticmethod
def _unpack_args(header=None, body=None, **kwargs):
@@ -1422,7 +1445,6 @@ def run(self, header, body, partial_args, app=None, interval=None,
app = app or self._get_app(body)
group_id = header.options.get('task_id') or uuid()
root_id = body.options.get('root_id')
- body.chord_size = self.__length_hint__()
options = dict(self.options, **options) if options else self.options
if options:
options.pop('task_id', None)
@@ -1436,11 +1458,11 @@ def run(self, header, body, partial_args, app=None, interval=None,
options.pop('chord', None)
options.pop('task_id', None)
- header_result = header.freeze(group_id=group_id, chord=body, root_id=root_id)
+ header_result_args = header._freeze_group_tasks(group_id=group_id, chord=body, root_id=root_id)
- if len(header_result) > 0:
+ if header.tasks:
app.backend.apply_chord(
- header_result,
+ header_result_args,
body,
interval=interval,
countdown=countdown,
@@ -1452,6 +1474,7 @@ def run(self, header, body, partial_args, app=None, interval=None,
# we execute the body manually here.
else:
body.delay([])
+ header_result = self.app.GroupResult(*header_result_args)
bodyres.parent = header_result
return bodyres
@@ -1504,7 +1527,7 @@ def _get_app(self, body=None):
tasks = self.tasks.tasks # is a group
except AttributeError:
tasks = self.tasks
- if len(tasks):
+ if tasks:
app = tasks[0]._app
if app is None and body is not None:
app = body._app
diff --git a/celery/utils/functional.py b/celery/utils/functional.py
index ab36e3d4c3d..3ff29c97993 100644
--- a/celery/utils/functional.py
+++ b/celery/utils/functional.py
@@ -3,7 +3,7 @@
import sys
from collections import UserList
from functools import partial
-from itertools import islice
+from itertools import islice, tee, zip_longest
from kombu.utils.functional import (LRUCache, dictfilter, is_list, lazy,
maybe_evaluate, maybe_list, memoize)
@@ -160,6 +160,19 @@ def uniq(it):
return (seen.add(obj) or obj for obj in it if obj not in seen)
+def lookahead(it):
+ """Yield pairs of (current, next) items in `it`.
+
+ `next` is None if `current` is the last item.
+ Example:
+ >>> list(lookahead(x for x in range(6)))
+ [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, None)]
+ """
+ a, b = tee(it)
+ next(b, None)
+ return zip_longest(a, b)
+
+
def regen(it):
"""Convert iterator to an object that can be consumed multiple times.
diff --git a/t/integration/tasks.py b/t/integration/tasks.py
index 2b4937a3725..2898d8a5ac7 100644
--- a/t/integration/tasks.py
+++ b/t/integration/tasks.py
@@ -24,6 +24,14 @@ def add(x, y, z=None):
return x + y
+@shared_task
+def write_to_file_and_return_int(file_name, i):
+ with open(file_name, mode='a', buffering=1) as file_handle:
+ file_handle.write(str(i)+'\n')
+
+ return i
+
+
@shared_task(typing=False)
def add_not_typed(x, y):
"""Add two numbers, but don't check arguments"""
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 2c96aa95b44..da57ac0c084 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -1,4 +1,5 @@
import re
+import tempfile
from datetime import datetime, timedelta
from time import sleep
@@ -18,7 +19,8 @@
print_unicode, raise_error, redis_echo,
replace_with_chain, replace_with_chain_which_raises,
replace_with_empty_chain, retry_once, return_exception,
- return_priority, second_order_replace1, tsum)
+ return_priority, second_order_replace1, tsum,
+ write_to_file_and_return_int)
RETRYABLE_EXCEPTIONS = (OSError, ConnectionError, TimeoutError)
@@ -1068,6 +1070,22 @@ def test_chord_on_error(self, manager):
assert len([cr for cr in chord_results if cr[2] != states.SUCCESS]
) == 1
+ @flaky
+ def test_generator(self, manager):
+ def assert_generator(file_name):
+ for i in range(3):
+ sleep(1)
+ if i == 2:
+ with open(file_name) as file_handle:
+ # ensures chord header generators tasks are processed incrementally #3021
+ assert file_handle.readline() == '0\n', "Chord header was unrolled too early"
+ yield write_to_file_and_return_int.s(file_name, i)
+
+ with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp_file:
+ file_name = tmp_file.name
+ c = chord(assert_generator(file_name), tsum.s())
+ assert c().get(timeout=TIMEOUT) == 3
+
@flaky
def test_parallel_chords(self, manager):
try:
diff --git a/t/unit/backends/test_base.py b/t/unit/backends/test_base.py
index 6f54bdf37f1..6cdb32d985a 100644
--- a/t/unit/backends/test_base.py
+++ b/t/unit/backends/test_base.py
@@ -189,26 +189,26 @@ def test_on_chord_part_return(self):
def test_apply_chord(self, unlock='celery.chord_unlock'):
self.app.tasks[unlock] = Mock()
- header_result = self.app.GroupResult(
+ header_result_args = (
uuid(),
[self.app.AsyncResult(x) for x in range(3)],
)
- self.b.apply_chord(header_result, self.callback.s())
+ self.b.apply_chord(header_result_args, self.callback.s())
assert self.app.tasks[unlock].apply_async.call_count
def test_chord_unlock_queue(self, unlock='celery.chord_unlock'):
self.app.tasks[unlock] = Mock()
- header_result = self.app.GroupResult(
+ header_result_args = (
uuid(),
[self.app.AsyncResult(x) for x in range(3)],
)
body = self.callback.s()
- self.b.apply_chord(header_result, body)
+ self.b.apply_chord(header_result_args, body)
called_kwargs = self.app.tasks[unlock].apply_async.call_args[1]
assert called_kwargs['queue'] is None
- self.b.apply_chord(header_result, body.set(queue='test_queue'))
+ self.b.apply_chord(header_result_args, body.set(queue='test_queue'))
called_kwargs = self.app.tasks[unlock].apply_async.call_args[1]
assert called_kwargs['queue'] == 'test_queue'
@@ -216,7 +216,7 @@ def test_chord_unlock_queue(self, unlock='celery.chord_unlock'):
def callback_queue(result):
pass
- self.b.apply_chord(header_result, callback_queue.s())
+ self.b.apply_chord(header_result_args, callback_queue.s())
called_kwargs = self.app.tasks[unlock].apply_async.call_args[1]
assert called_kwargs['queue'] == 'test_queue_two'
@@ -860,15 +860,15 @@ def test_restore_group_from_pickle(self):
def test_chord_apply_fallback(self):
self.b.implements_incr = False
self.b.fallback_chord_unlock = Mock()
- header_result = self.app.GroupResult(
+ header_result_args = (
'group_id',
[self.app.AsyncResult(x) for x in range(3)],
)
self.b.apply_chord(
- header_result, 'body', foo=1,
+ header_result_args, 'body', foo=1,
)
self.b.fallback_chord_unlock.assert_called_with(
- header_result, 'body', foo=1,
+ self.app.GroupResult(*header_result_args), 'body', foo=1,
)
def test_get_missing_meta(self):
diff --git a/t/unit/backends/test_cache.py b/t/unit/backends/test_cache.py
index 8400729017d..9e1ac5d29e4 100644
--- a/t/unit/backends/test_cache.py
+++ b/t/unit/backends/test_cache.py
@@ -71,12 +71,12 @@ def test_mark_as_failure(self):
def test_apply_chord(self):
tb = CacheBackend(backend='memory://', app=self.app)
- result = self.app.GroupResult(
+ result_args = (
uuid(),
[self.app.AsyncResult(uuid()) for _ in range(3)],
)
- tb.apply_chord(result, None)
- assert self.app.GroupResult.restore(result.id, backend=tb) == result
+ tb.apply_chord(result_args, None)
+ assert self.app.GroupResult.restore(result_args[0], backend=tb) == self.app.GroupResult(*result_args)
@patch('celery.result.GroupResult.restore')
def test_on_chord_part_return(self, restore):
@@ -91,12 +91,12 @@ def test_on_chord_part_return(self, restore):
self.app.tasks['foobarbaz'] = task
task.request.chord = signature(task)
- result = self.app.GroupResult(
+ result_args = (
uuid(),
[self.app.AsyncResult(uuid()) for _ in range(3)],
)
- task.request.group = result.id
- tb.apply_chord(result, None)
+ task.request.group = result_args[0]
+ tb.apply_chord(result_args, None)
deps.join_native.assert_not_called()
tb.on_chord_part_return(task.request, 'SUCCESS', 10)
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index fb236426f06..b4067345682 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -14,6 +14,7 @@
from celery.canvas import Signature
from celery.exceptions import (BackendStoreError, ChordError,
ImproperlyConfigured)
+from celery.result import AsyncResult, GroupResult
from celery.utils.collections import AttributeDict
@@ -296,7 +297,7 @@ def create_task(self, i, group_id="group_id"):
self.app.tasks['foobarbaz'] = task
task.request.chord = signature(task)
task.request.id = tid
- task.request.chord['chord_size'] = 10
+ self.b.set_chord_size(group_id, 10)
task.request.group = group_id
task.request.group_index = i
return task
@@ -306,7 +307,8 @@ def chord_context(self, size=1):
with patch('celery.backends.redis.maybe_signature') as ms:
request = Mock(name='request')
request.id = 'id1'
- request.group = 'gid1'
+ group_id = 'gid1'
+ request.group = group_id
request.group_index = None
tasks = [
self.create_task(i, group_id=request.group)
@@ -314,7 +316,7 @@ def chord_context(self, size=1):
]
callback = ms.return_value = Signature('add')
callback.id = 'id1'
- callback['chord_size'] = size
+ self.b.set_chord_size(group_id, size)
callback.delay = Mock(name='callback.delay')
yield tasks, request, callback
@@ -591,11 +593,11 @@ def test_expire(self):
def test_apply_chord(self, unlock='celery.chord_unlock'):
self.app.tasks[unlock] = Mock()
- header_result = self.app.GroupResult(
+ header_result_args = (
uuid(),
[self.app.AsyncResult(x) for x in range(3)],
)
- self.b.apply_chord(header_result, None)
+ self.b.apply_chord(header_result_args, None)
assert self.app.tasks[unlock].apply_async.call_count == 0
def test_unpack_chord_result(self):
@@ -640,6 +642,12 @@ def test_add_to_chord(self):
b.add_to_chord(gid, 'sig')
b.client.incr.assert_called_with(b.get_key_for_group(gid, '.t'), 1)
+ def test_set_chord_size(self):
+ b = self.Backend('redis://', app=self.app)
+ gid = uuid()
+ b.set_chord_size(gid, 10)
+ b.client.set.assert_called_with(b.get_key_for_group(gid, '.s'), 10)
+
def test_expires_is_None(self):
b = self.Backend(expires=None, app=self.app)
assert b.expires == self.app.conf.result_expires.total_seconds()
@@ -700,9 +708,10 @@ def test_on_chord_part_return(self):
assert self.b.client.zrangebyscore.call_count
jkey = self.b.get_key_for_group('group_id', '.j')
tkey = self.b.get_key_for_group('group_id', '.t')
- self.b.client.delete.assert_has_calls([call(jkey), call(tkey)])
+ skey = self.b.get_key_for_group('group_id', '.s')
+ self.b.client.delete.assert_has_calls([call(jkey), call(tkey), call(skey)])
self.b.client.expire.assert_has_calls([
- call(jkey, 86400), call(tkey, 86400),
+ call(jkey, 86400), call(tkey, 86400), call(skey, 86400),
])
def test_on_chord_part_return__unordered(self):
@@ -749,6 +758,7 @@ def test_on_chord_part_return_no_expiry(self):
old_expires = self.b.expires
self.b.expires = None
tasks = [self.create_task(i) for i in range(10)]
+ self.b.set_chord_size('group_id', 10)
for i in range(10):
self.b.on_chord_part_return(tasks[i].request, states.SUCCESS, i)
@@ -889,8 +899,8 @@ def test_on_chord_part_return__ChordError(self):
with self.chord_context(1) as (_, request, callback):
self.b.client.pipeline = ContextMock()
raise_on_second_call(self.b.client.pipeline, ChordError())
- self.b.client.pipeline.return_value.zadd().zcount().get().expire(
- ).expire().execute.return_value = (1, 1, 0, 4, 5)
+ self.b.client.pipeline.return_value.zadd().zcount().get().get().expire(
+ ).expire().expire().execute.return_value = (1, 1, 0, b'1', 4, 5, 6)
task = self.app._tasks['add'] = Mock(name='add_task')
self.b.on_chord_part_return(request, states.SUCCESS, 10)
task.backend.fail_from_current_stack.assert_called_with(
@@ -905,8 +915,8 @@ def test_on_chord_part_return__ChordError__unordered(self):
with self.chord_context(1) as (_, request, callback):
self.b.client.pipeline = ContextMock()
raise_on_second_call(self.b.client.pipeline, ChordError())
- self.b.client.pipeline.return_value.rpush().llen().get().expire(
- ).expire().execute.return_value = (1, 1, 0, 4, 5)
+ self.b.client.pipeline.return_value.rpush().llen().get().get().expire(
+ ).expire().expire().execute.return_value = (1, 1, 0, b'1', 4, 5, 6)
task = self.app._tasks['add'] = Mock(name='add_task')
self.b.on_chord_part_return(request, states.SUCCESS, 10)
task.backend.fail_from_current_stack.assert_called_with(
@@ -921,8 +931,8 @@ def test_on_chord_part_return__ChordError__ordered(self):
with self.chord_context(1) as (_, request, callback):
self.b.client.pipeline = ContextMock()
raise_on_second_call(self.b.client.pipeline, ChordError())
- self.b.client.pipeline.return_value.zadd().zcount().get().expire(
- ).expire().execute.return_value = (1, 1, 0, 4, 5)
+ self.b.client.pipeline.return_value.zadd().zcount().get().get().expire(
+ ).expire().expire().execute.return_value = (1, 1, 0, b'1', 4, 5, 6)
task = self.app._tasks['add'] = Mock(name='add_task')
self.b.on_chord_part_return(request, states.SUCCESS, 10)
task.backend.fail_from_current_stack.assert_called_with(
@@ -933,8 +943,8 @@ def test_on_chord_part_return__other_error(self):
with self.chord_context(1) as (_, request, callback):
self.b.client.pipeline = ContextMock()
raise_on_second_call(self.b.client.pipeline, RuntimeError())
- self.b.client.pipeline.return_value.zadd().zcount().get().expire(
- ).expire().execute.return_value = (1, 1, 0, 4, 5)
+ self.b.client.pipeline.return_value.zadd().zcount().get().get().expire(
+ ).expire().expire().execute.return_value = (1, 1, 0, b'1', 4, 5, 6)
task = self.app._tasks['add'] = Mock(name='add_task')
self.b.on_chord_part_return(request, states.SUCCESS, 10)
task.backend.fail_from_current_stack.assert_called_with(
@@ -949,8 +959,8 @@ def test_on_chord_part_return__other_error__unordered(self):
with self.chord_context(1) as (_, request, callback):
self.b.client.pipeline = ContextMock()
raise_on_second_call(self.b.client.pipeline, RuntimeError())
- self.b.client.pipeline.return_value.rpush().llen().get().expire(
- ).expire().execute.return_value = (1, 1, 0, 4, 5)
+ self.b.client.pipeline.return_value.rpush().llen().get().get().expire(
+ ).expire().expire().execute.return_value = (1, 1, 0, b'1', 4, 5, 6)
task = self.app._tasks['add'] = Mock(name='add_task')
self.b.on_chord_part_return(request, states.SUCCESS, 10)
task.backend.fail_from_current_stack.assert_called_with(
@@ -965,8 +975,8 @@ def test_on_chord_part_return__other_error__ordered(self):
with self.chord_context(1) as (_, request, callback):
self.b.client.pipeline = ContextMock()
raise_on_second_call(self.b.client.pipeline, RuntimeError())
- self.b.client.pipeline.return_value.zadd().zcount().get().expire(
- ).expire().execute.return_value = (1, 1, 0, 4, 5)
+ self.b.client.pipeline.return_value.zadd().zcount().get().get().expire(
+ ).expire().expire().execute.return_value = (1, 1, 0, b'1', 4, 5, 6)
task = self.app._tasks['add'] = Mock(name='add_task')
self.b.on_chord_part_return(request, states.SUCCESS, 10)
task.backend.fail_from_current_stack.assert_called_with(
@@ -980,42 +990,34 @@ def complex_header_result(self):
with patch("celery.result.GroupResult.restore") as p:
yield p
- def test_apply_chord_complex_header(self):
- mock_header_result = Mock()
+ @pytest.mark.parametrize(['results', 'assert_save_called'], [
# No results in the header at all - won't call `save()`
- mock_header_result.results = tuple()
- self.b.apply_chord(mock_header_result, None)
- mock_header_result.save.assert_not_called()
- mock_header_result.save.reset_mock()
- # A single simple result in the header - won't call `save()`
- mock_header_result.results = (self.app.AsyncResult("foo"), )
- self.b.apply_chord(mock_header_result, None)
- mock_header_result.save.assert_not_called()
- mock_header_result.save.reset_mock()
+ (tuple(), False),
+ # Simple results in the header - won't call `save()`
+ ((AsyncResult("foo"), ), False),
# Many simple results in the header - won't call `save()`
- mock_header_result.results = (self.app.AsyncResult("foo"), ) * 42
- self.b.apply_chord(mock_header_result, None)
- mock_header_result.save.assert_not_called()
- mock_header_result.save.reset_mock()
+ ((AsyncResult("foo"), ) * 42, False),
# A single complex result in the header - will call `save()`
- mock_header_result.results = (self.app.GroupResult("foo"), )
- self.b.apply_chord(mock_header_result, None)
- mock_header_result.save.assert_called_once_with(backend=self.b)
- mock_header_result.save.reset_mock()
+ ((GroupResult("foo", []),), True),
# Many complex results in the header - will call `save()`
- mock_header_result.results = (self.app.GroupResult("foo"), ) * 42
- self.b.apply_chord(mock_header_result, None)
- mock_header_result.save.assert_called_once_with(backend=self.b)
- mock_header_result.save.reset_mock()
+ ((GroupResult("foo"), ) * 42, True),
# Mixed simple and complex results in the header - will call `save()`
- mock_header_result.results = itertools.islice(
+ (itertools.islice(
itertools.cycle((
- self.app.AsyncResult("foo"), self.app.GroupResult("foo"),
+ AsyncResult("foo"), GroupResult("foo"),
)), 42,
- )
- self.b.apply_chord(mock_header_result, None)
- mock_header_result.save.assert_called_once_with(backend=self.b)
- mock_header_result.save.reset_mock()
+ ), True),
+ ])
+ def test_apply_chord_complex_header(self, results, assert_save_called):
+ mock_group_result = Mock()
+ mock_group_result.return_value.results = results
+ self.app.GroupResult = mock_group_result
+ header_result_args = ("gid11", results)
+ self.b.apply_chord(header_result_args, None)
+ if assert_save_called:
+ mock_group_result.return_value.save.assert_called_once_with(backend=self.b)
+ else:
+ mock_group_result.return_value.save.assert_not_called()
def test_on_chord_part_return_timeout(self, complex_header_result):
tasks = [self.create_task(i) for i in range(10)]
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
index b6bd7f94cea..6f638d04262 100644
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -785,6 +785,17 @@ def test_kwargs_delay_partial(self):
class test_chord(CanvasCase):
+ def test__get_app_does_not_exhaust_generator(self):
+ def build_generator():
+ yield self.add.s(1, 1)
+ self.second_item_returned = True
+ yield self.add.s(2, 2)
+
+ self.second_item_returned = False
+ c = chord(build_generator(), self.add.s(3))
+ c.app
+ assert not self.second_item_returned
+
def test_reverse(self):
x = chord([self.add.s(2, 2), self.add.s(4, 4)], body=self.mul.s(4))
assert isinstance(signature(x), chord)
diff --git a/t/unit/tasks/test_chord.py b/t/unit/tasks/test_chord.py
index f4e03a0e130..d977418c1bc 100644
--- a/t/unit/tasks/test_chord.py
+++ b/t/unit/tasks/test_chord.py
@@ -342,3 +342,13 @@ def test_run(self):
Chord(group(self.add.signature((i, i)) for i in range(5)), body)
Chord([self.add.signature((j, j)) for j in range(5)], body)
assert self.app.backend.apply_chord.call_count == 2
+
+ @patch('celery.Celery.backend', new=PropertyMock(name='backend'))
+ def test_run__chord_size_set(self):
+ Chord = self.app.tasks['celery.chord']
+ body = self.add.signature()
+ group_size = 4
+ group1 = group(self.add.signature((i, i)) for i in range(group_size))
+ result = Chord(group1, body)
+
+ self.app.backend.set_chord_size.assert_called_once_with(result.parent.id, group_size)
diff --git a/t/unit/utils/test_functional.py b/t/unit/utils/test_functional.py
index 2100b074000..54a89fd2551 100644
--- a/t/unit/utils/test_functional.py
+++ b/t/unit/utils/test_functional.py
@@ -3,8 +3,8 @@
from celery.utils.functional import (DummyContext, first, firstmethod,
fun_accepts_kwargs, fun_takes_argument,
- head_from_fun, maybe_list, mlazy,
- padlist, regen, seq_concat_item,
+ head_from_fun, lookahead, maybe_list,
+ mlazy, padlist, regen, seq_concat_item,
seq_concat_seq)
@@ -66,6 +66,10 @@ def predicate(value):
assert iterations[0] == 10
+def test_lookahead():
+ assert list(lookahead(x for x in range(6))) == [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, None)]
+
+
def test_maybe_list():
assert maybe_list(1) == [1]
assert maybe_list([1]) == [1]
@@ -136,14 +140,12 @@ def test_gen__negative_index(self, g):
def test_nonzero__does_not_consume_more_than_first_item(self):
def build_generator():
yield 1
- self.consumed_second_item = True
+ pytest.fail("generator should not consume past first item")
yield 2
- self.consumed_second_item = False
g = regen(build_generator())
assert bool(g)
assert g[0] == 1
- assert not self.consumed_second_item
def test_nonzero__empty_iter(self):
assert not regen(iter([]))
From bf5b2bceffd52707bcde0fe30935182ba96e3c80 Mon Sep 17 00:00:00 2001
From: David Schneider
Date: Thu, 4 Mar 2021 16:02:09 +0100
Subject: [PATCH 167/415] Forward shadow option for retried tasks (#6655)
Co-authored-by: David Schneider
---
celery/app/task.py | 2 ++
t/unit/tasks/test_tasks.py | 6 ++++++
2 files changed, 8 insertions(+)
diff --git a/celery/app/task.py b/celery/app/task.py
index 1d4c974cb22..3e386822bec 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -74,6 +74,7 @@ class Context:
headers = None
delivery_info = None
reply_to = None
+ shadow = None
root_id = None
parent_id = None
correlation_id = None
@@ -114,6 +115,7 @@ def as_execution_options(self):
'parent_id': self.parent_id,
'group_id': self.group,
'group_index': self.group_index,
+ 'shadow': self.shadow,
'chord': self.chord,
'chain': self.chain,
'link': self.callbacks,
diff --git a/t/unit/tasks/test_tasks.py b/t/unit/tasks/test_tasks.py
index 900d7decdbc..7ac83ed5243 100644
--- a/t/unit/tasks/test_tasks.py
+++ b/t/unit/tasks/test_tasks.py
@@ -422,6 +422,12 @@ def test_signature_from_request__delivery_info(self):
assert sig.options['exchange'] == 'testex'
assert sig.options['routing_key'] == 'testrk'
+ def test_signature_from_request__shadow_name(self):
+ self.retry_task.push_request()
+ self.retry_task.request.shadow = 'test'
+ sig = self.retry_task.signature_from_request()
+ assert sig.options['shadow'] == 'test'
+
def test_retry_kwargs_can_be_empty(self):
self.retry_task_mockapply.push_request()
try:
From 8a4056087aeac6a5be79a2db4d6f06975f754609 Mon Sep 17 00:00:00 2001
From: Matt <30868661+namloc2001@users.noreply.github.com>
Date: Thu, 4 Mar 2021 15:20:55 +0000
Subject: [PATCH 168/415] Update platforms.py "superuser privileges" check
(#6600)
* Update platforms.py
Making it so that the check for "superuser privileges" is only looking for uid/euid=0, not including the root group (gid/egid=0) which does not have the superuser privileges associated with uid/euid=0.
* Update platforms.py
* Update platforms.py
* Update platforms.py
* Update platforms.py
* Update celery/platforms.py
Co-authored-by: Omer Katz
* Update celery/platforms.py
Co-authored-by: Omer Katz
* Update celery/platforms.py
Co-authored-by: Omer Katz
* Update celery/platforms.py
Co-authored-by: Omer Katz
* Update platforms.py
* Update celery/platforms.py
Co-authored-by: Omer Katz
* Update platforms.py
* Update platforms.py
* Update celery/platforms.py
Co-authored-by: Omer Katz
* Update platforms.py
* Refactor contribution.
* Basic tests.
* Catch the SecurityError and exit the program.
* More tests.
* Ensure no warnings are present.
* Cover the case where the platform doesn't have fchown available.
* Test that a security error is raised when suspicious group names are used.
* Remove unused import.
Co-authored-by: Omer Katz
---
celery/bin/worker.py | 79 +++++++------
celery/exceptions.py | 8 +-
celery/platforms.py | 87 ++++++++++----
t/unit/utils/test_platforms.py | 208 ++++++++++++++++++++++++++++++---
4 files changed, 300 insertions(+), 82 deletions(-)
diff --git a/celery/bin/worker.py b/celery/bin/worker.py
index ca16a19b4e3..e38b86fb16b 100644
--- a/celery/bin/worker.py
+++ b/celery/bin/worker.py
@@ -11,6 +11,7 @@
from celery.bin.base import (COMMA_SEPARATED_LIST, LOG_LEVEL,
CeleryDaemonCommand, CeleryOption,
handle_preload_options)
+from celery.exceptions import SecurityError
from celery.platforms import (EX_FAILURE, EX_OK, detached,
maybe_drop_privileges)
from celery.utils.log import get_logger
@@ -289,40 +290,44 @@ def worker(ctx, hostname=None, pool_cls=None, app=None, uid=None, gid=None,
$ celery worker --autoscale=10,0
"""
- app = ctx.obj.app
- if ctx.args:
- try:
- app.config_from_cmdline(ctx.args, namespace='worker')
- except (KeyError, ValueError) as e:
- # TODO: Improve the error messages
- raise click.UsageError(
- "Unable to parse extra configuration from command line.\n"
- f"Reason: {e}", ctx=ctx)
- if kwargs.get('detach', False):
- argv = ['-m', 'celery'] + sys.argv[1:]
- if '--detach' in argv:
- argv.remove('--detach')
- if '-D' in argv:
- argv.remove('-D')
-
- return detach(sys.executable,
- argv,
- logfile=logfile,
- pidfile=pidfile,
- uid=uid, gid=gid,
- umask=kwargs.get('umask', None),
- workdir=kwargs.get('workdir', None),
- app=app,
- executable=kwargs.get('executable', None),
- hostname=hostname)
-
- maybe_drop_privileges(uid=uid, gid=gid)
- worker = app.Worker(
- hostname=hostname, pool_cls=pool_cls, loglevel=loglevel,
- logfile=logfile, # node format handled by celery.app.log.setup
- pidfile=node_format(pidfile, hostname),
- statedb=node_format(statedb, hostname),
- no_color=ctx.obj.no_color,
- **kwargs)
- worker.start()
- return worker.exitcode
+ try:
+ app = ctx.obj.app
+ if ctx.args:
+ try:
+ app.config_from_cmdline(ctx.args, namespace='worker')
+ except (KeyError, ValueError) as e:
+ # TODO: Improve the error messages
+ raise click.UsageError(
+ "Unable to parse extra configuration from command line.\n"
+ f"Reason: {e}", ctx=ctx)
+ if kwargs.get('detach', False):
+ argv = ['-m', 'celery'] + sys.argv[1:]
+ if '--detach' in argv:
+ argv.remove('--detach')
+ if '-D' in argv:
+ argv.remove('-D')
+
+ return detach(sys.executable,
+ argv,
+ logfile=logfile,
+ pidfile=pidfile,
+ uid=uid, gid=gid,
+ umask=kwargs.get('umask', None),
+ workdir=kwargs.get('workdir', None),
+ app=app,
+ executable=kwargs.get('executable', None),
+ hostname=hostname)
+
+ maybe_drop_privileges(uid=uid, gid=gid)
+ worker = app.Worker(
+ hostname=hostname, pool_cls=pool_cls, loglevel=loglevel,
+ logfile=logfile, # node format handled by celery.app.log.setup
+ pidfile=node_format(pidfile, hostname),
+ statedb=node_format(statedb, hostname),
+ no_color=ctx.obj.no_color,
+ **kwargs)
+ worker.start()
+ return worker.exitcode
+ except SecurityError as e:
+ ctx.obj.error(e.args[0])
+ ctx.exit(1)
diff --git a/celery/exceptions.py b/celery/exceptions.py
index 66b3ca2a341..f40c7c29b9e 100644
--- a/celery/exceptions.py
+++ b/celery/exceptions.py
@@ -44,6 +44,7 @@
- :class:`~celery.exceptions.DuplicateNodenameWarning`
- :class:`~celery.exceptions.FixupWarning`
- :class:`~celery.exceptions.NotConfigured`
+ - :class:`~celery.exceptions.SecurityWarning`
- :exc:`BaseException`
- :exc:`SystemExit`
- :exc:`~celery.exceptions.WorkerTerminate`
@@ -62,7 +63,7 @@
# Warnings
'CeleryWarning',
'AlwaysEagerIgnored', 'DuplicateNodenameWarning',
- 'FixupWarning', 'NotConfigured',
+ 'FixupWarning', 'NotConfigured', 'SecurityWarning',
# Core errors
'CeleryError',
@@ -128,6 +129,11 @@ class NotConfigured(CeleryWarning):
"""Celery hasn't been configured, as no config module has been found."""
+class SecurityWarning(CeleryWarning):
+ """Potential security issue found."""
+ pass
+
+
class CeleryError(Exception):
"""Base class for all Celery errors."""
diff --git a/celery/platforms.py b/celery/platforms.py
index 452435be6ac..83392a20e83 100644
--- a/celery/platforms.py
+++ b/celery/platforms.py
@@ -6,6 +6,7 @@
import atexit
import errno
+import grp
import math
import numbers
import os
@@ -21,7 +22,7 @@
from kombu.utils.compat import maybe_fileno
from kombu.utils.encoding import safe_str
-from .exceptions import SecurityError, reraise
+from .exceptions import SecurityError, SecurityWarning, reraise
from .local import try_import
try:
@@ -66,8 +67,6 @@
_range = namedtuple('_range', ('start', 'stop'))
-C_FORCE_ROOT = os.environ.get('C_FORCE_ROOT', False)
-
ROOT_DISALLOWED = """\
Running a worker with superuser privileges when the
worker accepts messages serialized with pickle is a very bad idea!
@@ -87,6 +86,11 @@
User information: uid={uid} euid={euid} gid={gid} egid={egid}
"""
+ASSUMING_ROOT = """\
+An entry for the specified gid or egid was not found.
+We're assuming this is a potential security issue.
+"""
+
SIGNAMES = {
sig for sig in dir(_signal)
if sig.startswith('SIG') and '_' not in sig
@@ -146,6 +150,7 @@ def acquire(self):
except OSError as exc:
reraise(LockFailed, LockFailed(str(exc)), sys.exc_info()[2])
return self
+
__enter__ = acquire
def is_locked(self):
@@ -155,6 +160,7 @@ def is_locked(self):
def release(self, *args):
"""Release lock."""
self.remove()
+
__exit__ = release
def read_pid(self):
@@ -346,17 +352,19 @@ def open(self):
mputil._run_after_forkers()
self._is_open = True
+
__enter__ = open
def close(self, *args):
if self._is_open:
self._is_open = False
+
__exit__ = close
def _detach(self):
- if os.fork() == 0: # first child
- os.setsid() # create new session
- if os.fork() > 0: # pragma: no cover
+ if os.fork() == 0: # first child
+ os.setsid() # create new session
+ if os.fork() > 0: # pragma: no cover
# second child
os._exit(0)
else:
@@ -463,7 +471,7 @@ def _setgroups_hack(groups):
while 1:
try:
return os.setgroups(groups)
- except ValueError: # error from Python's check.
+ except ValueError: # error from Python's check.
if len(groups) <= 1:
raise
groups[:] = groups[:-1]
@@ -625,7 +633,7 @@ def arm_alarm(self, seconds): # noqa
_signal.alarm(math.ceil(seconds))
else: # pragma: no cover
- def arm_alarm(self, seconds): # noqa
+ def arm_alarm(self, seconds): # noqa
return _itimer_alarm(seconds) # noqa
def reset_alarm(self):
@@ -688,10 +696,10 @@ def update(self, _d_=None, **sigmap):
signals = Signals()
-get_signal = signals.signum # compat
+get_signal = signals.signum # compat
install_signal_handler = signals.__setitem__ # compat
-reset_signal = signals.reset # compat
-ignore_signal = signals.ignore # compat
+reset_signal = signals.reset # compat
+ignore_signal = signals.ignore # compat
def signal_name(signum):
@@ -772,6 +780,9 @@ def ignore_errno(*errnos, **kwargs):
def check_privileges(accept_content):
+ pickle_or_serialize = ('pickle' in accept_content
+ or 'application/group-python-serialize' in accept_content)
+
uid = os.getuid() if hasattr(os, 'getuid') else 65535
gid = os.getgid() if hasattr(os, 'getgid') else 65535
euid = os.geteuid() if hasattr(os, 'geteuid') else 65535
@@ -779,20 +790,46 @@ def check_privileges(accept_content):
if hasattr(os, 'fchown'):
if not all(hasattr(os, attr)
- for attr in ['getuid', 'getgid', 'geteuid', 'getegid']):
+ for attr in ('getuid', 'getgid', 'geteuid', 'getegid')):
raise SecurityError('suspicious platform, contact support')
- if not uid or not gid or not euid or not egid:
- if ('pickle' in accept_content or
- 'application/x-python-serialize' in accept_content):
- if not C_FORCE_ROOT:
- try:
- print(ROOT_DISALLOWED.format(
- uid=uid, euid=euid, gid=gid, egid=egid,
- ), file=sys.stderr)
- finally:
- sys.stderr.flush()
- os._exit(1)
- warnings.warn(RuntimeWarning(ROOT_DISCOURAGED.format(
+ # Get the group database entry for the current user's group and effective
+ # group id using grp.getgrgid() method
+ # We must handle the case where either the gid or the egid are not found.
+ try:
+ gid_entry = grp.getgrgid(gid)
+ egid_entry = grp.getgrgid(egid)
+ except KeyError:
+ warnings.warn(SecurityWarning(ASSUMING_ROOT))
+ _warn_or_raise_security_error(egid, euid, gid, uid,
+ pickle_or_serialize)
+ return
+
+ # Get the group and effective group name based on gid
+ gid_grp_name = gid_entry[0]
+ egid_grp_name = egid_entry[0]
+
+ # Create lists to use in validation step later.
+ gids_in_use = (gid_grp_name, egid_grp_name)
+ groups_with_security_risk = ('sudo', 'wheel')
+
+ is_root = uid == 0 or euid == 0
+ # Confirm that the gid and egid are not one that
+ # can be used to escalate privileges.
+ if is_root or any(group in gids_in_use
+ for group in groups_with_security_risk):
+ _warn_or_raise_security_error(egid, euid, gid, uid,
+ pickle_or_serialize)
+
+
+def _warn_or_raise_security_error(egid, euid, gid, uid, pickle_or_serialize):
+ c_force_root = os.environ.get('C_FORCE_ROOT', False)
+
+ if pickle_or_serialize and not c_force_root:
+ raise SecurityError(ROOT_DISALLOWED.format(
uid=uid, euid=euid, gid=gid, egid=egid,
- )))
+ ))
+
+ warnings.warn(SecurityWarning(ROOT_DISCOURAGED.format(
+ uid=uid, euid=euid, gid=gid, egid=egid,
+ )))
diff --git a/t/unit/utils/test_platforms.py b/t/unit/utils/test_platforms.py
index c58a3ed6d68..cfb856f8c18 100644
--- a/t/unit/utils/test_platforms.py
+++ b/t/unit/utils/test_platforms.py
@@ -1,5 +1,6 @@
import errno
import os
+import re
import signal
import sys
import tempfile
@@ -10,9 +11,10 @@
import t.skip
from celery import _find_option_with_arg, platforms
-from celery.exceptions import SecurityError
-from celery.platforms import (DaemonContext, LockFailed, Pidfile,
- _setgroups_hack, check_privileges,
+from celery.exceptions import SecurityError, SecurityWarning
+from celery.platforms import (ASSUMING_ROOT, ROOT_DISALLOWED,
+ ROOT_DISCOURAGED, DaemonContext, LockFailed,
+ Pidfile, _setgroups_hack, check_privileges,
close_open_fds, create_pidlock, detached,
fd_by_path, get_fdmax, ignore_errno, initgroups,
isatty, maybe_drop_privileges, parse_gid,
@@ -158,6 +160,7 @@ def test_reset(self, set):
def test_setitem(self, set):
def handle(*args):
return args
+
signals['INT'] = handle
set.assert_called_with(signal.SIGINT, handle)
@@ -218,6 +221,7 @@ class pw_struct:
def raise_on_second_call(*args, **kwargs):
setuid.side_effect = OSError()
setuid.side_effect.errno = errno.EPERM
+
setuid.side_effect = raise_on_second_call
getpwuid.return_value = pw_struct()
parse_uid.return_value = 5001
@@ -237,7 +241,9 @@ def to_root_on_second_call(mock, first):
def on_first_call(*args, **kwargs):
ret, return_value[0] = return_value[0], 0
return ret
+
mock.side_effect = on_first_call
+
to_root_on_second_call(geteuid, 10)
to_root_on_second_call(getuid, 10)
with pytest.raises(SecurityError):
@@ -259,6 +265,7 @@ def on_first_call(*args, **kwargs):
def raise_on_second_call(*args, **kwargs):
setuid.side_effect = OSError()
setuid.side_effect.errno = errno.ENOENT
+
setuid.side_effect = raise_on_second_call
with pytest.raises(OSError):
maybe_drop_privileges(uid='user')
@@ -274,6 +281,7 @@ def test_with_guid(self, initgroups, setuid, setgid,
def raise_on_second_call(*args, **kwargs):
setuid.side_effect = OSError()
setuid.side_effect.errno = errno.EPERM
+
setuid.side_effect = raise_on_second_call
parse_uid.return_value = 5001
parse_gid.return_value = 50001
@@ -327,7 +335,6 @@ def test_parse_uid_when_int(self):
@patch('pwd.getpwnam')
def test_parse_uid_when_existing_name(self, getpwnam):
-
class pwent:
pw_uid = 5001
@@ -346,7 +353,6 @@ def test_parse_gid_when_int(self):
@patch('grp.getgrnam')
def test_parse_gid_when_existing_name(self, getgrnam):
-
class grent:
gr_gid = 50001
@@ -739,6 +745,7 @@ def on_setgroups(groups):
setgroups.return_value = True
return
raise ValueError()
+
setgroups.side_effect = on_setgroups
_setgroups_hack(list(range(400)))
@@ -756,6 +763,7 @@ def on_setgroups(groups):
setgroups.return_value = True
return
raise exc
+
setgroups.side_effect = on_setgroups
_setgroups_hack(list(range(400)))
@@ -817,17 +825,179 @@ def test_setgroups_raises_EPERM(self, hack, getgroups):
getgroups.assert_called_with()
-def test_check_privileges():
- class Obj:
- fchown = 13
- prev, platforms.os = platforms.os, Obj()
- try:
- with pytest.raises(SecurityError):
- check_privileges({'pickle'})
- finally:
- platforms.os = prev
- prev, platforms.os = platforms.os, object()
- try:
- check_privileges({'pickle'})
- finally:
- platforms.os = prev
+@pytest.mark.parametrize('accept_content', [
+ {'pickle'},
+ {'application/group-python-serialize'},
+ {'pickle', 'application/group-python-serialize'}
+])
+def test_check_privileges_suspicious_platform(accept_content):
+ with patch('celery.platforms.os') as os_module:
+ del os_module.getuid
+ del os_module.getgid
+ del os_module.geteuid
+ del os_module.getegid
+
+ with pytest.raises(SecurityError,
+ match=r'suspicious platform, contact support'):
+ check_privileges(accept_content)
+
+
+@pytest.mark.parametrize('accept_content', [
+ {'pickle'},
+ {'application/group-python-serialize'},
+ {'pickle', 'application/group-python-serialize'}
+])
+def test_check_privileges(accept_content, recwarn):
+ check_privileges(accept_content)
+
+ assert len(recwarn) == 0
+
+
+@pytest.mark.parametrize('accept_content', [
+ {'pickle'},
+ {'application/group-python-serialize'},
+ {'pickle', 'application/group-python-serialize'}
+])
+def test_check_privileges_no_fchown(accept_content, recwarn):
+ with patch('celery.platforms.os') as os_module:
+ del os_module.fchown
+ check_privileges(accept_content)
+
+ assert len(recwarn) == 0
+
+
+@pytest.mark.parametrize('accept_content', [
+ {'pickle'},
+ {'application/group-python-serialize'},
+ {'pickle', 'application/group-python-serialize'}
+])
+def test_check_privileges_without_c_force_root(accept_content):
+ with patch('celery.platforms.os') as os_module:
+ os_module.environ = {}
+ os_module.getuid.return_value = 0
+ os_module.getgid.return_value = 0
+ os_module.geteuid.return_value = 0
+ os_module.getegid.return_value = 0
+
+ expected_message = re.escape(ROOT_DISALLOWED.format(uid=0, euid=0,
+ gid=0, egid=0))
+ with pytest.raises(SecurityError,
+ match=expected_message):
+ check_privileges(accept_content)
+
+
+@pytest.mark.parametrize('accept_content', [
+ {'pickle'},
+ {'application/group-python-serialize'},
+ {'pickle', 'application/group-python-serialize'}
+])
+def test_check_privileges_with_c_force_root(accept_content):
+ with patch('celery.platforms.os') as os_module:
+ os_module.environ = {'C_FORCE_ROOT': 'true'}
+ os_module.getuid.return_value = 0
+ os_module.getgid.return_value = 0
+ os_module.geteuid.return_value = 0
+ os_module.getegid.return_value = 0
+
+ with pytest.warns(SecurityWarning):
+ check_privileges(accept_content)
+
+
+@pytest.mark.parametrize(('accept_content', 'group_name'), [
+ ({'pickle'}, 'sudo'),
+ ({'application/group-python-serialize'}, 'sudo'),
+ ({'pickle', 'application/group-python-serialize'}, 'sudo'),
+ ({'pickle'}, 'wheel'),
+ ({'application/group-python-serialize'}, 'wheel'),
+ ({'pickle', 'application/group-python-serialize'}, 'wheel'),
+])
+def test_check_privileges_with_c_force_root_and_with_suspicious_group(accept_content, group_name):
+ with patch('celery.platforms.os') as os_module, patch('celery.platforms.grp') as grp_module:
+ os_module.environ = {'C_FORCE_ROOT': 'true'}
+ os_module.getuid.return_value = 60
+ os_module.getgid.return_value = 60
+ os_module.geteuid.return_value = 60
+ os_module.getegid.return_value = 60
+
+ grp_module.getgrgid.return_value = [group_name]
+ grp_module.getgrgid.return_value = [group_name]
+
+ expected_message = re.escape(ROOT_DISCOURAGED.format(uid=60, euid=60,
+ gid=60, egid=60))
+ with pytest.warns(SecurityWarning, match=expected_message):
+ check_privileges(accept_content)
+
+
+@pytest.mark.parametrize(('accept_content', 'group_name'), [
+ ({'pickle'}, 'sudo'),
+ ({'application/group-python-serialize'}, 'sudo'),
+ ({'pickle', 'application/group-python-serialize'}, 'sudo'),
+ ({'pickle'}, 'wheel'),
+ ({'application/group-python-serialize'}, 'wheel'),
+ ({'pickle', 'application/group-python-serialize'}, 'wheel'),
+])
+def test_check_privileges_without_c_force_root_and_with_suspicious_group(accept_content, group_name):
+ with patch('celery.platforms.os') as os_module, patch('celery.platforms.grp') as grp_module:
+ os_module.environ = {}
+ os_module.getuid.return_value = 60
+ os_module.getgid.return_value = 60
+ os_module.geteuid.return_value = 60
+ os_module.getegid.return_value = 60
+
+ grp_module.getgrgid.return_value = [group_name]
+ grp_module.getgrgid.return_value = [group_name]
+
+ expected_message = re.escape(ROOT_DISALLOWED.format(uid=60, euid=60,
+ gid=60, egid=60))
+ with pytest.raises(SecurityError,
+ match=expected_message):
+ check_privileges(accept_content)
+
+
+@pytest.mark.parametrize('accept_content', [
+ {'pickle'},
+ {'application/group-python-serialize'},
+ {'pickle', 'application/group-python-serialize'}
+])
+def test_check_privileges_with_c_force_root_and_no_group_entry(accept_content, recwarn):
+ with patch('celery.platforms.os') as os_module, patch('celery.platforms.grp') as grp_module:
+ os_module.environ = {'C_FORCE_ROOT': 'true'}
+ os_module.getuid.return_value = 60
+ os_module.getgid.return_value = 60
+ os_module.geteuid.return_value = 60
+ os_module.getegid.return_value = 60
+
+ grp_module.getgrgid.side_effect = KeyError
+
+ expected_message = ROOT_DISCOURAGED.format(uid=60, euid=60,
+ gid=60, egid=60)
+
+ check_privileges(accept_content)
+ assert len(recwarn) == 2
+
+ assert recwarn[0].message.args[0] == ASSUMING_ROOT
+ assert recwarn[1].message.args[0] == expected_message
+
+
+@pytest.mark.parametrize('accept_content', [
+ {'pickle'},
+ {'application/group-python-serialize'},
+ {'pickle', 'application/group-python-serialize'}
+])
+def test_check_privileges_with_c_force_root_and_no_group_entry(accept_content, recwarn):
+ with patch('celery.platforms.os') as os_module, patch('celery.platforms.grp') as grp_module:
+ os_module.environ = {}
+ os_module.getuid.return_value = 60
+ os_module.getgid.return_value = 60
+ os_module.geteuid.return_value = 60
+ os_module.getegid.return_value = 60
+
+ grp_module.getgrgid.side_effect = KeyError
+
+ expected_message = re.escape(ROOT_DISALLOWED.format(uid=60, euid=60,
+ gid=60, egid=60))
+ with pytest.raises(SecurityError,
+ match=expected_message):
+ check_privileges(accept_content)
+
+ assert recwarn[0].message.args[0] == ASSUMING_ROOT
From 6e558062494b2185747b58292541996b73609fb7 Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Thu, 4 Mar 2021 22:44:11 +0600
Subject: [PATCH 169/415] attempt to fix #6517 (#6599)
Co-authored-by: Omer Katz
---
celery/bin/worker.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/celery/bin/worker.py b/celery/bin/worker.py
index e38b86fb16b..5b5e7fd8ed3 100644
--- a/celery/bin/worker.py
+++ b/celery/bin/worker.py
@@ -325,6 +325,7 @@ def worker(ctx, hostname=None, pool_cls=None, app=None, uid=None, gid=None,
pidfile=node_format(pidfile, hostname),
statedb=node_format(statedb, hostname),
no_color=ctx.obj.no_color,
+ quiet=ctx.obj.quiet,
**kwargs)
worker.start()
return worker.exitcode
From 671c1235bd71e716ea7982fe587d21779c34cb02 Mon Sep 17 00:00:00 2001
From: LaughInJar <287464+LaughInJar@users.noreply.github.com>
Date: Thu, 4 Mar 2021 17:44:47 +0100
Subject: [PATCH 170/415] remove unused property `autoregister` from the Task
class (#6624)
issue: https://github.com/celery/celery/issues/6623
Seems to have been an remainder from 3.x times when class
based tasks have autoregistered.
Co-authored-by: Simon Lachinger
---
celery/app/task.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/celery/app/task.py b/celery/app/task.py
index 3e386822bec..801eba11a8f 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -221,9 +221,6 @@ class Task:
#: The result store backend used for this task.
backend = None
- #: If disabled this task won't be registered automatically.
- autoregister = True
-
#: If enabled the task will report its status as 'started' when the task
#: is executed by a worker. Disabled by default as the normal behavior
#: is to not report that level of granularity. Tasks are either pending,
From 7c4a48e5e7a40c7e0c0e1170e9c807c98f738c11 Mon Sep 17 00:00:00 2001
From: Sergey Lyapustin
Date: Thu, 4 Mar 2021 20:47:19 +0200
Subject: [PATCH 171/415] Fixed typos
---
celery/backends/redis.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index d684c196fd7..ef2badf72b4 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -48,13 +48,13 @@
W_REDIS_SSL_CERT_OPTIONAL = """
Setting ssl_cert_reqs=CERT_OPTIONAL when connecting to redis means that \
-celery might not valdate the identity of the redis broker when connecting. \
+celery might not validate the identity of the redis broker when connecting. \
This leaves you vulnerable to man in the middle attacks.
"""
W_REDIS_SSL_CERT_NONE = """
Setting ssl_cert_reqs=CERT_NONE when connecting to redis means that celery \
-will not valdate the identity of the redis broker when connecting. This \
+will not validate the identity of the redis broker when connecting. This \
leaves you vulnerable to man in the middle attacks.
"""
From f091bab758cf430640678a16a759892bdd352800 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 7 Mar 2021 18:31:20 +0200
Subject: [PATCH 172/415] Run CI when dependencies change & when the pipeline
itself changes (#6663)
* Run CI when dependencies change.
* Run CI when the pipeline itself changes.
---
.github/workflows/python-package.yml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index af1ffb27f4f..34fc435ddef 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -8,10 +8,14 @@ on:
branches: [ master ]
paths:
- '**.py'
+ - '**.txt'
+ - '.github/workflows/python-package.yml'
pull_request:
branches: [ master ]
paths:
- '**.py'
+ - '**.txt'
+ - '.github/workflows/python-package.yml'
jobs:
build:
From 1ff70592523f83fc8096fa208f03a826d7d13756 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 9 Mar 2021 18:36:19 +0200
Subject: [PATCH 173/415] fnmatch.translate() already translates globs for us.
(#6668)
There's no longer a need for our faulty implementation of glob_to_re since the functionality is provided in the fnmatch standard module.
This incidently, fixes #5646.
---
celery/app/routes.py | 10 +++-------
t/unit/app/test_routes.py | 10 ++++++++--
2 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/celery/app/routes.py b/celery/app/routes.py
index 348c8880351..a56ce59e00b 100644
--- a/celery/app/routes.py
+++ b/celery/app/routes.py
@@ -2,8 +2,8 @@
Contains utilities for working with task routers, (:setting:`task_routes`).
"""
+import fnmatch
import re
-import string
from collections import OrderedDict
from collections.abc import Mapping
@@ -23,11 +23,6 @@
__all__ = ('MapRoute', 'Router', 'prepare')
-def glob_to_re(glob, quote=string.punctuation.replace('*', '')):
- glob = ''.join('\\' + c if c in quote else c for c in glob)
- return glob.replace('*', '.+?')
-
-
class MapRoute:
"""Creates a router out of a :class:`dict`."""
@@ -39,7 +34,7 @@ def __init__(self, map):
if isinstance(k, Pattern):
self.patterns[k] = v
elif '*' in k:
- self.patterns[re.compile(glob_to_re(k))] = v
+ self.patterns[re.compile(fnmatch.translate(k))] = v
else:
self.map[k] = v
@@ -126,6 +121,7 @@ def expand_router_string(router):
def prepare(routes):
"""Expand the :setting:`task_routes` setting."""
+
def expand_route(route):
if isinstance(route, (Mapping, list, tuple)):
return MapRoute(route)
diff --git a/t/unit/app/test_routes.py b/t/unit/app/test_routes.py
index 309335e1923..20d49be87df 100644
--- a/t/unit/app/test_routes.py
+++ b/t/unit/app/test_routes.py
@@ -16,6 +16,7 @@ def Router(app, *args, **kwargs):
def E(app, queues):
def expand(answer):
return Router(app, [], queues).expand_destination(answer)
+
return expand
@@ -46,6 +47,7 @@ def setup(self):
@self.app.task(shared=False)
def mytask(*args, **kwargs):
pass
+
self.mytask = mytask
def assert_routes_to_queue(self, queue, router, name,
@@ -56,7 +58,8 @@ def assert_routes_to_queue(self, queue, router, name,
kwargs = {}
if args is None:
args = []
- assert router.route(options, name, args, kwargs)['queue'].name == queue
+ assert router.route(options, name, args, kwargs)[
+ 'queue'].name == queue
def assert_routes_to_default_queue(self, router, name, *args, **kwargs):
self.assert_routes_to_queue(
@@ -85,10 +88,13 @@ def test_route_for_task__glob(self):
from re import compile
route = routes.MapRoute([
+ ('proj.tasks.bar*', {'queue': 'routeC'}),
('proj.tasks.*', 'routeA'),
('demoapp.tasks.bar.*', {'exchange': 'routeB'}),
(compile(r'(video|image)\.tasks\..*'), {'queue': 'media'}),
])
+ assert route('proj.tasks.bar') == {'queue': 'routeC'}
+ assert route('proj.tasks.bar.baz') == {'queue': 'routeC'}
assert route('proj.tasks.foo') == {'queue': 'routeA'}
assert route('demoapp.tasks.bar.moo') == {'exchange': 'routeB'}
assert route('video.tasks.foo') == {'queue': 'media'}
@@ -97,7 +103,7 @@ def test_route_for_task__glob(self):
def test_expand_route_not_found(self):
expand = E(self.app, self.app.amqp.Queues(
- self.app.conf.task_queues, False))
+ self.app.conf.task_queues, False))
route = routes.MapRoute({'a': {'queue': 'x'}})
with pytest.raises(QueueNotFound):
expand(route('a'))
From 1d42c78e763aedcba456c616f0bb2f49d9193df1 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 10 Mar 2021 13:45:11 +0200
Subject: [PATCH 174/415] Upgrade syntax to Python 3.6+.
---
celery/canvas.py | 4 ++--
t/integration/tasks.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/celery/canvas.py b/celery/canvas.py
index 47afd2be9bc..57b0aea0628 100644
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -1041,9 +1041,9 @@ def from_dict(cls, d, app=None):
# access elements from that dictionary later and refer to objects
# canonicalized here
orig_tasks = d["kwargs"]["tasks"]
- d["kwargs"]["tasks"] = rebuilt_tasks = type(orig_tasks)((
+ d["kwargs"]["tasks"] = rebuilt_tasks = type(orig_tasks)(
maybe_signature(task, app=app) for task in orig_tasks
- ))
+ )
return group(rebuilt_tasks, app=app, **d['options'])
def __init__(self, *tasks, **options):
diff --git a/t/integration/tasks.py b/t/integration/tasks.py
index 2898d8a5ac7..4e88bcd880a 100644
--- a/t/integration/tasks.py
+++ b/t/integration/tasks.py
@@ -363,7 +363,7 @@ def rebuild_signature(sig_dict):
def _recurse(sig):
if not isinstance(sig, Signature):
- raise TypeError("{!r} is not a signature object".format(sig))
+ raise TypeError(f"{sig!r} is not a signature object")
# Most canvas types have a `tasks` attribute
if isinstance(sig, (chain, group, chord)):
for task in sig.tasks:
From c8f95f6a5e8b988ef022137608e45c533473fbcb Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 10 Mar 2021 13:46:56 +0200
Subject: [PATCH 175/415] Remove redundant pass statement.
---
celery/backends/redis.py | 1 -
celery/exceptions.py | 1 -
2 files changed, 2 deletions(-)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index ef2badf72b4..aa3c13d114d 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -567,7 +567,6 @@ class SentinelManagedSSLConnection(
SSL Connection.
"""
- pass
class SentinelBackend(RedisBackend):
diff --git a/celery/exceptions.py b/celery/exceptions.py
index f40c7c29b9e..a30f460c69a 100644
--- a/celery/exceptions.py
+++ b/celery/exceptions.py
@@ -131,7 +131,6 @@ class NotConfigured(CeleryWarning):
class SecurityWarning(CeleryWarning):
"""Potential security issue found."""
- pass
class CeleryError(Exception):
From 47af16889fb14e4aeda7b3dec74c521a7b5b1b40 Mon Sep 17 00:00:00 2001
From: Ruaridh Williamson
Date: Sat, 13 Mar 2021 04:30:29 +0000
Subject: [PATCH 176/415] Add `azureblockblob_base_path` config (#6669)
* Add `azureblockblob_base_path` config
- Allow for a basepath such as 'FolderName/' within the Azure container
* Docs for `azureblockblob_base_path`
* Add Azure basepath to defaults
* Add unit tests of Azure base path
* Update Contributors
* Add `versionadded` to docs of Azure basepath
* Fix example path
* Add tests for base_path conf
* Update celery/backends/azureblockblob.py
use a fstring
Co-authored-by: Omer Katz
* Update celery/backends/azureblockblob.py
use a fstring
Co-authored-by: Omer Katz
* Update celery/backends/azureblockblob.py
use a fstring
Co-authored-by: Omer Katz
* Update docs/userguide/configuration.rst
Co-authored-by: Omer Katz
Co-authored-by: Asif Saif Uddin
Co-authored-by: Omer Katz
---
CONTRIBUTORS.txt | 1 +
celery/app/defaults.py | 1 +
celery/backends/azureblockblob.py | 8 ++++--
docs/userguide/configuration.rst | 13 +++++++++
t/unit/backends/test_azureblockblob.py | 39 ++++++++++++++++++++------
5 files changed, 51 insertions(+), 11 deletions(-)
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 7cf4b9a60bb..38f1cb8f09d 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -280,3 +280,4 @@ Maksym Shalenyi, 2020/07/30
Frazer McLean, 2020/09/29
Henrik Bruåsdal, 2020/11/29
Tom Wojcik, 2021/01/24
+Ruaridh Williamson, 2021/03/09
diff --git a/celery/app/defaults.py b/celery/app/defaults.py
index 51e1e2f96c1..fcf147f3cdc 100644
--- a/celery/app/defaults.py
+++ b/celery/app/defaults.py
@@ -132,6 +132,7 @@ def __repr__(self):
retry_initial_backoff_sec=Option(2, type='int'),
retry_increment_base=Option(2, type='int'),
retry_max_attempts=Option(3, type='int'),
+ base_path=Option('', type='string'),
),
control=Namespace(
queue_ttl=Option(300.0, type='float'),
diff --git a/celery/backends/azureblockblob.py b/celery/backends/azureblockblob.py
index 81b15f6dec0..972baaf73e9 100644
--- a/celery/backends/azureblockblob.py
+++ b/celery/backends/azureblockblob.py
@@ -43,6 +43,8 @@ def __init__(self,
container_name or
conf["azureblockblob_container_name"])
+ self.base_path = conf.get('azureblockblob_base_path', '')
+
@classmethod
def _parse_url(cls, url, prefix="azureblockblob://"):
connection_string = url[len(prefix):]
@@ -82,7 +84,7 @@ def get(self, key):
blob_client = self._blob_service_client.get_blob_client(
container=self._container_name,
- blob=key,
+ blob=f'{self.base_path}{key}',
)
try:
@@ -103,7 +105,7 @@ def set(self, key, value):
blob_client = self._blob_service_client.get_blob_client(
container=self._container_name,
- blob=key,
+ blob=f'{self.base_path}{key}',
)
blob_client.upload_blob(value, overwrite=True)
@@ -129,7 +131,7 @@ def delete(self, key):
blob_client = self._blob_service_client.get_blob_client(
container=self._container_name,
- blob=key,
+ blob=f'{self.base_path}{key}',
)
blob_client.delete_blob()
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index cf85255ec54..8ff0c8f809e 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -1529,6 +1529,19 @@ Default: celery.
The name for the storage container in which to store the results.
+.. setting:: azureblockblob_base_path
+
+``azureblockblob_base_path``
+~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 5.1
+
+Default: None.
+
+A base path in the storage container to use to store result keys. For example::
+
+ azureblockblob_base_path = 'prefix/'
+
.. setting:: azureblockblob_retry_initial_backoff_sec
``azureblockblob_retry_initial_backoff_sec``
diff --git a/t/unit/backends/test_azureblockblob.py b/t/unit/backends/test_azureblockblob.py
index 596764bc174..46c3c77222e 100644
--- a/t/unit/backends/test_azureblockblob.py
+++ b/t/unit/backends/test_azureblockblob.py
@@ -25,6 +25,10 @@ def setup(self):
app=self.app,
url=self.url)
+ @pytest.fixture(params=['', 'my_folder/'])
+ def base_path(self, request):
+ return request.param
+
def test_missing_third_party_sdk(self):
azurestorage = azureblockblob.azurestorage
try:
@@ -57,11 +61,12 @@ def test_create_client(self, mock_blob_service_factory):
assert mock_blob_service_client_instance.create_container.call_count == 1
@patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._blob_service_client")
- def test_get(self, mock_client):
+ def test_get(self, mock_client, base_path):
+ self.backend.base_path = base_path
self.backend.get(b"mykey")
mock_client.get_blob_client \
- .assert_called_once_with(blob="mykey", container="celery")
+ .assert_called_once_with(blob=base_path + "mykey", container="celery")
mock_client.get_blob_client.return_value \
.download_blob.return_value \
@@ -77,31 +82,49 @@ def test_get_missing(self, mock_client):
assert self.backend.get(b"mykey") is None
@patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._blob_service_client")
- def test_set(self, mock_client):
+ def test_set(self, mock_client, base_path):
+ self.backend.base_path = base_path
self.backend._set_with_state(b"mykey", "myvalue", states.SUCCESS)
mock_client.get_blob_client.assert_called_once_with(
- container="celery", blob="mykey")
+ container="celery", blob=base_path + "mykey")
mock_client.get_blob_client.return_value \
.upload_blob.assert_called_once_with("myvalue", overwrite=True)
@patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._blob_service_client")
- def test_mget(self, mock_client):
+ def test_mget(self, mock_client, base_path):
keys = [b"mykey1", b"mykey2"]
+ self.backend.base_path = base_path
self.backend.mget(keys)
mock_client.get_blob_client.assert_has_calls(
- [call(blob=key.decode(), container='celery') for key in keys],
+ [call(blob=base_path + key.decode(), container='celery') for key in keys],
any_order=True,)
@patch(MODULE_TO_MOCK + ".AzureBlockBlobBackend._blob_service_client")
- def test_delete(self, mock_client):
+ def test_delete(self, mock_client, base_path):
+ self.backend.base_path = base_path
self.backend.delete(b"mykey")
mock_client.get_blob_client.assert_called_once_with(
- container="celery", blob="mykey")
+ container="celery", blob=base_path + "mykey")
mock_client.get_blob_client.return_value \
.delete_blob.assert_called_once()
+
+ def test_base_path_conf(self, base_path):
+ self.app.conf.azureblockblob_base_path = base_path
+ backend = AzureBlockBlobBackend(
+ app=self.app,
+ url=self.url
+ )
+ assert backend.base_path == base_path
+
+ def test_base_path_conf_default(self):
+ backend = AzureBlockBlobBackend(
+ app=self.app,
+ url=self.url
+ )
+ assert backend.base_path == ''
From a0998a330cc01b5e498cdad20370a734b64dcc75 Mon Sep 17 00:00:00 2001
From: careljonkhout
Date: Sat, 13 Mar 2021 13:35:29 +0100
Subject: [PATCH 177/415] document options time_limit options of apply_async
I have noticed that time_limit and soft_time_limit do actually work with apply_async. Since I would like to use these options in my project, it would give me some peace of mind if these options were explicitly documented.
---
celery/app/task.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/celery/app/task.py b/celery/app/task.py
index 801eba11a8f..0fef1324e06 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -454,6 +454,11 @@ def apply_async(self, args=None, kwargs=None, task_id=None, producer=None,
retry_policy (Mapping): Override the retry policy used.
See the :setting:`task_publish_retry_policy` setting.
+
+ time_limit (int): If set, overrides the default time limit.
+
+ soft_time_limit (int): If set, overrides the default soft
+ time limit.
queue (str, kombu.Queue): The queue to route the task to.
This must be a key present in :setting:`task_queues`, or
From 4d77ddddb10797011dc10dd2e4e1e7a7467b8431 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 11 Mar 2021 13:31:52 +0200
Subject: [PATCH 178/415] Drop the lzma extra.
The lzma backport is no longer needed since we don't support Python<3.3 anymore.
---
requirements/extras/lzma.txt | 1 -
setup.py | 1 -
2 files changed, 2 deletions(-)
delete mode 100644 requirements/extras/lzma.txt
diff --git a/requirements/extras/lzma.txt b/requirements/extras/lzma.txt
deleted file mode 100644
index 9c70afdf861..00000000000
--- a/requirements/extras/lzma.txt
+++ /dev/null
@@ -1 +0,0 @@
-backports.lzma;python_version<"3.3"
diff --git a/setup.py b/setup.py
index d4e27c1226e..9022141035e 100644
--- a/setup.py
+++ b/setup.py
@@ -27,7 +27,6 @@
'eventlet',
'gevent',
'librabbitmq',
- 'lzma',
'memcache',
'mongodb',
'msgpack',
From 1f3c98149bc791874063c870048067d3a0f2c674 Mon Sep 17 00:00:00 2001
From: Illia Volochii
Date: Mon, 15 Mar 2021 18:49:23 +0200
Subject: [PATCH 179/415] Fix checking expiration of X.509 certificates (#6678)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`not_valid_after` is a naïve datetime representing a moment in UTC. It should not be compared to a naïve datetime representing the current local date and time.
Also, the value is inclusive.
https://cryptography.io/en/3.4.6/x509/reference.html#cryptography.x509.Certificate.not_valid_after
---
celery/security/certificate.py | 2 +-
t/unit/security/test_certificate.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/celery/security/certificate.py b/celery/security/certificate.py
index fc4961cec74..0f3fd8680f7 100644
--- a/celery/security/certificate.py
+++ b/celery/security/certificate.py
@@ -27,7 +27,7 @@ def __init__(self, cert):
def has_expired(self):
"""Check if the certificate has expired."""
- return datetime.datetime.now() > self._cert.not_valid_after
+ return datetime.datetime.utcnow() >= self._cert.not_valid_after
def get_pubkey(self):
"""Get public key from certificate."""
diff --git a/t/unit/security/test_certificate.py b/t/unit/security/test_certificate.py
index a52980422e8..910cb624618 100644
--- a/t/unit/security/test_certificate.py
+++ b/t/unit/security/test_certificate.py
@@ -38,7 +38,7 @@ def test_has_expired_mock(self):
x = Certificate(CERT1)
x._cert = Mock(name='cert')
- time_after = datetime.datetime.now() + datetime.timedelta(days=-1)
+ time_after = datetime.datetime.utcnow() + datetime.timedelta(days=-1)
x._cert.not_valid_after = time_after
assert x.has_expired() is True
@@ -47,7 +47,7 @@ def test_has_not_expired_mock(self):
x = Certificate(CERT1)
x._cert = Mock(name='cert')
- time_after = datetime.datetime.now() + datetime.timedelta(days=1)
+ time_after = datetime.datetime.utcnow() + datetime.timedelta(days=1)
x._cert.not_valid_after = time_after
assert x.has_expired() is False
From 8cfe4a59843ba63876e6c6147dd8b9561254c920 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pengjie=20Song=20=28=E5=AE=8B=E9=B9=8F=E6=8D=B7=29?=
Date: Tue, 16 Mar 2021 13:35:31 +0800
Subject: [PATCH 180/415] Fix JSON decoding errors when using MongoDB as
backend (#6675)
* Fix JSON decoding errors when using MongoDB as backend
'traceback' is a string. 'children' is a list. Both of them cannot be decoded by JSON.
* Add unit tests for PR #6675
---
celery/backends/mongodb.py | 4 ++--
t/unit/backends/test_mongodb.py | 15 ++++++++++++++-
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/celery/backends/mongodb.py b/celery/backends/mongodb.py
index 76eab766b75..60448663aa9 100644
--- a/celery/backends/mongodb.py
+++ b/celery/backends/mongodb.py
@@ -202,8 +202,8 @@ def _get_task_meta_for(self, task_id):
'status': obj['status'],
'result': self.decode(obj['result']),
'date_done': obj['date_done'],
- 'traceback': self.decode(obj['traceback']),
- 'children': self.decode(obj['children']),
+ 'traceback': obj['traceback'],
+ 'children': obj['children'],
})
return {'status': states.PENDING, 'result': None}
diff --git a/t/unit/backends/test_mongodb.py b/t/unit/backends/test_mongodb.py
index d0e651ed37c..8dd91eeba22 100644
--- a/t/unit/backends/test_mongodb.py
+++ b/t/unit/backends/test_mongodb.py
@@ -661,13 +661,26 @@ def test_encode_success_results(self, mongo_backend_factory, serializer,
assert type(recovered) == result_type
assert recovered == result
+ @pytest.mark.parametrize("serializer",
+ ["bson", "pickle", "yaml", "json", "msgpack"])
+ def test_encode_chain_results(self, mongo_backend_factory, serializer):
+ backend = mongo_backend_factory(serializer=serializer)
+ mock_request = MagicMock(spec=['children'])
+ children = [self.app.AsyncResult(uuid()) for i in range(10)]
+ mock_request.children = children
+ backend.store_result(TASK_ID, 0, 'SUCCESS', request=mock_request)
+ recovered = backend.get_children(TASK_ID)
+ def tuple_to_list(t): return [list(t[0]), t[1]]
+ assert recovered == [tuple_to_list(c.as_tuple()) for c in children]
+
@pytest.mark.parametrize("serializer",
["bson", "pickle", "yaml", "json", "msgpack"])
def test_encode_exception_error_results(self, mongo_backend_factory,
serializer):
backend = mongo_backend_factory(serializer=serializer)
exception = Exception("Basic Exception")
- backend.store_result(TASK_ID, exception, 'FAILURE')
+ traceback = 'Traceback:\n Exception: Basic Exception\n'
+ backend.store_result(TASK_ID, exception, 'FAILURE', traceback)
recovered = backend.get_result(TASK_ID)
assert type(recovered) == type(exception)
assert recovered.args == exception.args
From 45044954379e39d2600d48da41b67f8f1c0b3dc4 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 16 Mar 2021 13:32:55 +0200
Subject: [PATCH 181/415] Skip tests if client library is not available.
---
t/unit/backends/test_azureblockblob.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/t/unit/backends/test_azureblockblob.py b/t/unit/backends/test_azureblockblob.py
index 46c3c77222e..7c80400cc1e 100644
--- a/t/unit/backends/test_azureblockblob.py
+++ b/t/unit/backends/test_azureblockblob.py
@@ -10,6 +10,7 @@
MODULE_TO_MOCK = "celery.backends.azureblockblob"
pytest.importorskip('azure.storage.blob')
+pytest.importorskip('azure.core.exceptions')
class test_AzureBlockBlobBackend:
From 36e4452773dfc2ef79b8ff535908dee620aacef0 Mon Sep 17 00:00:00 2001
From: Chris Morris
Date: Tue, 16 Mar 2021 10:41:23 -0400
Subject: [PATCH 182/415] Allow configuration of RedisBackend's
health_check_interval (#6666)
* Allow configuration of RedisBackend health_check_interval
* Only add key if value is set
* Added documentation for the redis_backend_health_check_interval setting.
---
celery/backends/redis.py | 4 +++
docs/userguide/configuration.rst | 16 +++++++++++
t/unit/backends/test_redis.py | 48 ++++++++++++++++++++++++++++++++
3 files changed, 68 insertions(+)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index aa3c13d114d..d3805cfb429 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -219,6 +219,7 @@ def __init__(self, host=None, port=None, db=None, password=None,
socket_connect_timeout = _get('redis_socket_connect_timeout')
retry_on_timeout = _get('redis_retry_on_timeout')
socket_keepalive = _get('redis_socket_keepalive')
+ health_check_interval = _get('redis_backend_health_check_interval')
self.connparams = {
'host': _get('redis_host') or 'localhost',
@@ -232,6 +233,9 @@ def __init__(self, host=None, port=None, db=None, password=None,
socket_connect_timeout and float(socket_connect_timeout),
}
+ if health_check_interval:
+ self.connparams["health_check_interval"] = health_check_interval
+
# absent in redis.connection.UnixDomainSocketConnection
if socket_keepalive:
self.connparams['socket_keepalive'] = socket_keepalive
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index 8ff0c8f809e..e653b0d82d0 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -1185,6 +1185,22 @@ Note that the ``ssl_cert_reqs`` string should be one of ``required``,
``optional``, or ``none`` (though, for backwards compatibility, the string
may also be one of ``CERT_REQUIRED``, ``CERT_OPTIONAL``, ``CERT_NONE``).
+
+.. setting:: redis_backend_health_check_interval
+
+.. versionadded:: 5.1.0
+
+``redis_backend_health_check_interval``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Default: Not configured
+
+The Redis backend supports health checks. This value must be
+set as an integer whose value is the number of seconds between
+health checks. If a ConnectionError or a TimeoutError is
+encountered during the health check, the connection will be
+re-established and the command retried exactly once.
+
.. setting:: redis_backend_use_ssl
``redis_backend_use_ssl``
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index b4067345682..a33fce329ca 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -418,6 +418,54 @@ def test_backend_ssl(self):
from redis.connection import SSLConnection
assert x.connparams['connection_class'] is SSLConnection
+ def test_backend_health_check_interval_ssl(self):
+ pytest.importorskip('redis')
+
+ self.app.conf.redis_backend_use_ssl = {
+ 'ssl_cert_reqs': ssl.CERT_REQUIRED,
+ 'ssl_ca_certs': '/path/to/ca.crt',
+ 'ssl_certfile': '/path/to/client.crt',
+ 'ssl_keyfile': '/path/to/client.key',
+ }
+ self.app.conf.redis_backend_health_check_interval = 10
+ x = self.Backend(
+ 'rediss://:bosco@vandelay.com:123//1', app=self.app,
+ )
+ assert x.connparams
+ assert x.connparams['host'] == 'vandelay.com'
+ assert x.connparams['db'] == 1
+ assert x.connparams['port'] == 123
+ assert x.connparams['password'] == 'bosco'
+ assert x.connparams['health_check_interval'] == 10
+
+ from redis.connection import SSLConnection
+ assert x.connparams['connection_class'] is SSLConnection
+
+ def test_backend_health_check_interval(self):
+ pytest.importorskip('redis')
+
+ self.app.conf.redis_backend_health_check_interval = 10
+ x = self.Backend(
+ 'redis://vandelay.com:123//1', app=self.app,
+ )
+ assert x.connparams
+ assert x.connparams['host'] == 'vandelay.com'
+ assert x.connparams['db'] == 1
+ assert x.connparams['port'] == 123
+ assert x.connparams['health_check_interval'] == 10
+
+ def test_backend_health_check_interval_not_set(self):
+ pytest.importorskip('redis')
+
+ x = self.Backend(
+ 'redis://vandelay.com:123//1', app=self.app,
+ )
+ assert x.connparams
+ assert x.connparams['host'] == 'vandelay.com'
+ assert x.connparams['db'] == 1
+ assert x.connparams['port'] == 123
+ assert "health_check_interval" not in x.connparams
+
@pytest.mark.parametrize('cert_str', [
"required",
"CERT_REQUIRED",
From f2556c45b22cae9d3c961639627a1f9053681393 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 17 Mar 2021 13:45:50 +0200
Subject: [PATCH 183/415] Add missing manager fixture.
---
t/integration/test_canvas.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index da57ac0c084..6b71e2284ab 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -1177,7 +1177,7 @@ def test_chord_in_chain_with_args(self, manager):
assert res1.get(timeout=TIMEOUT) == [1, 1]
@pytest.mark.xfail(reason="Issue #6200")
- def test_chain_in_chain_with_args(self):
+ def test_chain_in_chain_with_args(self, manager):
try:
manager.app.backend.ensure_chords_allowed()
except NotImplementedError as e:
From fc55f2afa4121567acc9217a0da065c293c1fb9e Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 17 Mar 2021 13:47:16 +0200
Subject: [PATCH 184/415] Fix typo.
---
t/integration/test_canvas.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 6b71e2284ab..2c19812f885 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -1252,7 +1252,7 @@ def test_nested_chord_group_chain_group_tail(self, manager):
Sanity check that a deeply nested group is completed as expected.
Groups at the end of chains nested in chords have had issues and this
- simple test sanity check that such a tsk structure can be completed.
+ simple test sanity check that such a task structure can be completed.
"""
try:
manager.app.backend.ensure_chords_allowed()
From aaef28ce1ac4d9c8b5b1f3f968c1095594213767 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 22 Mar 2021 07:00:55 +0200
Subject: [PATCH 185/415] Added testcase for issue #6437 (#6684)
* Added testcase for issue #6437.
* Add second test case.
---
t/integration/test_canvas.py | 28 +++++++++++++++++++++++++++-
1 file changed, 27 insertions(+), 1 deletion(-)
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 2c19812f885..afa81a053f2 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -11,7 +11,8 @@
from celery.result import AsyncResult, GroupResult, ResultSet
from . import tasks
-from .conftest import get_active_redis_channels, get_redis_connection
+from .conftest import get_active_redis_channels, get_redis_connection, \
+ TEST_BACKEND
from .tasks import (ExpectedException, add, add_chord_to_chord, add_replaced,
add_to_all, add_to_all_to_chord, build_chain_inside_task,
chord_error, collect_ids, delayed_sum,
@@ -1274,6 +1275,31 @@ def test_nested_chord_group_chain_group_tail(self, manager):
res = sig.delay()
assert res.get(timeout=TIMEOUT) == [[42, 42]]
+ @pytest.mark.xfail(TEST_BACKEND.startswith('redis://'), reason="Issue #6437")
+ def test_error_propagates_from_chord(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ sig = add.s(1, 1) | fail.s() | group(add.s(1), add.s(1))
+ res = sig.delay()
+
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+
+ def test_error_propagates_from_chord2(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ sig = add.s(1, 1) | add.s(1) | group(add.s(1), fail.s())
+ res = sig.delay()
+
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+
class test_signature_serialization:
"""
From 1c955bf76a9a33052d87f2bccf0889fb21f27d41 Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Mon, 22 Mar 2021 14:18:00 +0600
Subject: [PATCH 186/415] Safeguard against schedule entry without kwargs
(#6619)
* Possible fix for #5889
verification & test needed
* Update celery/beat.py
Co-authored-by: Omer Katz
* Extract logic to methods.
* Add test.
Co-authored-by: Omer Katz
---
celery/beat.py | 22 ++++++++++++++++++++--
t/unit/app/test_beat.py | 16 ++++++++++++++++
2 files changed, 36 insertions(+), 2 deletions(-)
diff --git a/celery/beat.py b/celery/beat.py
index 3e1d31a59ac..2b251e838a2 100644
--- a/celery/beat.py
+++ b/celery/beat.py
@@ -203,6 +203,24 @@ def __ne__(self, other):
return not self == other
+def _evaluate_entry_args(entry_args):
+ if not entry_args:
+ return []
+ return [
+ v() if isinstance(v, BeatLazyFunc) else v
+ for v in entry_args.args
+ ]
+
+
+def _evaluate_entry_kwargs(entry_kwargs):
+ if not entry_kwargs:
+ return {}
+ return {
+ k: v() if isinstance(v, BeatLazyFunc) else v
+ for k, v in entry_kwargs.items()
+ }
+
+
class Scheduler:
"""Scheduler for periodic tasks.
@@ -380,8 +398,8 @@ def apply_async(self, entry, producer=None, advance=True, **kwargs):
task = self.app.tasks.get(entry.task)
try:
- entry_args = [v() if isinstance(v, BeatLazyFunc) else v for v in (entry.args or [])]
- entry_kwargs = {k: v() if isinstance(v, BeatLazyFunc) else v for k, v in entry.kwargs.items()}
+ entry_args = _evaluate_entry_args(entry.args)
+ entry_kwargs = _evaluate_entry_kwargs(entry.kwargs)
if task:
return task.apply_async(entry_args, entry_kwargs,
producer=producer,
diff --git a/t/unit/app/test_beat.py b/t/unit/app/test_beat.py
index 4b8339f451b..98f4f21bf3f 100644
--- a/t/unit/app/test_beat.py
+++ b/t/unit/app/test_beat.py
@@ -196,6 +196,22 @@ def foo():
scheduler.apply_async(scheduler.Entry(task=foo.name, app=self.app, args=None, kwargs=None))
foo.apply_async.assert_called()
+ def test_apply_async_with_null_args_set_to_none(self):
+
+ @self.app.task(shared=False)
+ def foo():
+ pass
+ foo.apply_async = Mock(name='foo.apply_async')
+
+ scheduler = mScheduler(app=self.app)
+ entry = scheduler.Entry(task=foo.name, app=self.app, args=None,
+ kwargs=None)
+ entry.args = None
+ entry.kwargs = None
+
+ scheduler.apply_async(entry, advance=False)
+ foo.apply_async.assert_called()
+
def test_should_sync(self):
@self.app.task(shared=False)
From 6ffd82778ecc55a0f6feaf3fea89481b9b9dafc9 Mon Sep 17 00:00:00 2001
From: gal cohen
Date: Mon, 29 Mar 2021 11:36:40 +0300
Subject: [PATCH 187/415] Docs only - SQS broker - add STS support (#6693)
* add STS to docs
* Update docs/getting-started/backends-and-brokers/sqs.rst
Co-authored-by: Omer Katz
* Update docs/getting-started/backends-and-brokers/sqs.rst
Co-authored-by: Omer Katz
* add sts link
Co-authored-by: galcohen
Co-authored-by: Asif Saif Uddin
Co-authored-by: Omer Katz
---
.../backends-and-brokers/sqs.rst | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/docs/getting-started/backends-and-brokers/sqs.rst b/docs/getting-started/backends-and-brokers/sqs.rst
index 47ec6d8f864..cd8fd2a3b33 100644
--- a/docs/getting-started/backends-and-brokers/sqs.rst
+++ b/docs/getting-started/backends-and-brokers/sqs.rst
@@ -192,6 +192,29 @@ The above policy:
+-----------------------------------------+--------------------------------------------+
+STS token authentication
+----------------------------
+
+https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role.html
+
+AWS STS authentication is supported by using the ``sts_role_arn`` and ``sts_token_timeout`` broker transport options. ``sts_role_arn`` is the assumed IAM role ARN we use to authorize our access to SQS.
+``sts_token_timeout`` is the token timeout, defaults (and minimum) to 900 seconds. After the mentioned period, a new token will be created.
+
+ broker_transport_options = {
+ 'predefined_queues': {
+ 'my-q': {
+ 'url': 'https://ap-southeast-2.queue.amazonaws.com/123456/my-q',
+ 'access_key_id': 'xxx',
+ 'secret_access_key': 'xxx',
+ 'backoff_policy': {1: 10, 2: 20, 3: 40, 4: 80, 5: 320, 6: 640},
+ 'backoff_tasks': ['svc.tasks.tasks.task1']
+ }
+ },
+ 'sts_role_arn': 'arn:aws:iam:::role/STSTest', # optional
+ 'sts_token_timeout': 900 # optional
+ }
+
+
.. _sqs-caveats:
Caveats
From 9a3e56b99d7d810e11a14e20ecfe6db9162026f8 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 29 Mar 2021 13:35:32 +0300
Subject: [PATCH 188/415] Added a test for #5469.
---
t/unit/utils/test_functional.py | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/t/unit/utils/test_functional.py b/t/unit/utils/test_functional.py
index 54a89fd2551..c84e152385d 100644
--- a/t/unit/utils/test_functional.py
+++ b/t/unit/utils/test_functional.py
@@ -225,6 +225,29 @@ def f(cls, x):
fun = head_from_fun(A.f, bound=True)
assert fun(1) == 1
+ @pytest.mark.xfail(reason="Issue #5469")
+ def test_kwonly_required_args(self):
+ local = {}
+ fun = ('def f_kwargs_required(*, a="a", b, c=None):'
+ ' return')
+ exec(fun, {}, local)
+ f_kwargs_required = local['f_kwargs_required']
+ g = head_from_fun(f_kwargs_required)
+
+ with pytest.raises(TypeError):
+ g(1)
+
+ with pytest.raises(TypeError):
+ g(a=1)
+
+ with pytest.raises(TypeError):
+ g(b=1)
+
+ with pytest.raises(TypeError):
+ g(a=2, b=1)
+
+ g(b=3)
+
class test_fun_takes_argument:
From a78f8cc56a0c1f1536028f17f5ea900240a00816 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 29 Mar 2021 13:48:22 +0300
Subject: [PATCH 189/415] Fix test case for #5469.
---
t/unit/utils/test_functional.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/t/unit/utils/test_functional.py b/t/unit/utils/test_functional.py
index c84e152385d..0fc1f10511a 100644
--- a/t/unit/utils/test_functional.py
+++ b/t/unit/utils/test_functional.py
@@ -241,10 +241,10 @@ def test_kwonly_required_args(self):
g(a=1)
with pytest.raises(TypeError):
- g(b=1)
+ g(c=1)
with pytest.raises(TypeError):
- g(a=2, b=1)
+ g(a=2, c=1)
g(b=3)
From 4c58b56a94b579c0a879f9d628a950c469464c6e Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 29 Mar 2021 16:13:54 +0300
Subject: [PATCH 190/415] isort & autopep8.
---
celery/app/task.py | 4 ++--
celery/backends/redis.py | 1 -
t/integration/test_canvas.py | 4 ++--
t/unit/app/test_routes.py | 2 +-
4 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/celery/app/task.py b/celery/app/task.py
index 0fef1324e06..53dd79b21fc 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -454,9 +454,9 @@ def apply_async(self, args=None, kwargs=None, task_id=None, producer=None,
retry_policy (Mapping): Override the retry policy used.
See the :setting:`task_publish_retry_policy` setting.
-
+
time_limit (int): If set, overrides the default time limit.
-
+
soft_time_limit (int): If set, overrides the default soft
time limit.
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index d3805cfb429..74a2e18b582 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -572,7 +572,6 @@ class SentinelManagedSSLConnection(
"""
-
class SentinelBackend(RedisBackend):
"""Redis sentinel task result store."""
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index afa81a053f2..4c5f31a495f 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -11,8 +11,8 @@
from celery.result import AsyncResult, GroupResult, ResultSet
from . import tasks
-from .conftest import get_active_redis_channels, get_redis_connection, \
- TEST_BACKEND
+from .conftest import (TEST_BACKEND, get_active_redis_channels,
+ get_redis_connection)
from .tasks import (ExpectedException, add, add_chord_to_chord, add_replaced,
add_to_all, add_to_all_to_chord, build_chain_inside_task,
chord_error, collect_ids, delayed_sum,
diff --git a/t/unit/app/test_routes.py b/t/unit/app/test_routes.py
index 20d49be87df..fbb2803b4d1 100644
--- a/t/unit/app/test_routes.py
+++ b/t/unit/app/test_routes.py
@@ -59,7 +59,7 @@ def assert_routes_to_queue(self, queue, router, name,
if args is None:
args = []
assert router.route(options, name, args, kwargs)[
- 'queue'].name == queue
+ 'queue'].name == queue
def assert_routes_to_default_queue(self, router, name, *args, **kwargs):
self.assert_routes_to_queue(
From b863168ac9bc0811cbf73409d4101be02fe34489 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 29 Mar 2021 16:55:04 +0300
Subject: [PATCH 191/415] Drop fun_accepts_kwargs backport.
`inspect.signature` is available since Python 3.3.
---
celery/utils/functional.py | 26 ++++++++------------------
1 file changed, 8 insertions(+), 18 deletions(-)
diff --git a/celery/utils/functional.py b/celery/utils/functional.py
index 3ff29c97993..d9808d2237f 100644
--- a/celery/utils/functional.py
+++ b/celery/utils/functional.py
@@ -90,6 +90,7 @@ def firstmethod(method, on_call=None):
The list can also contain lazy instances
(:class:`~kombu.utils.functional.lazy`.)
"""
+
def _matcher(it, *args, **kwargs):
for obj in it:
try:
@@ -101,6 +102,7 @@ def _matcher(it, *args, **kwargs):
else:
if reply is not None:
return reply
+
return _matcher
@@ -327,24 +329,12 @@ def fun_takes_argument(name, fun, position=None):
)
-if hasattr(inspect, 'signature'):
- def fun_accepts_kwargs(fun):
- """Return true if function accepts arbitrary keyword arguments."""
- return any(
- p for p in inspect.signature(fun).parameters.values()
- if p.kind == p.VAR_KEYWORD
- )
-else:
- def fun_accepts_kwargs(fun): # noqa
- """Return true if function accepts arbitrary keyword arguments."""
- try:
- argspec = inspect.getargspec(fun)
- except TypeError:
- try:
- argspec = inspect.getargspec(fun.__call__)
- except (TypeError, AttributeError):
- return
- return not argspec or argspec[2] is not None
+def fun_accepts_kwargs(fun):
+ """Return true if function accepts arbitrary keyword arguments."""
+ return any(
+ p for p in inspect.signature(fun).parameters.values()
+ if p.kind == p.VAR_KEYWORD
+ )
def maybe(typ, val):
From 6157bc9053da6c1b149283d0ab58fe2958a8f2dd Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Tue, 30 Mar 2021 17:37:32 +0300
Subject: [PATCH 192/415] Tasks can now have required kwargs at any order.
(#6699)
Fixes #5469.
Thanks to @dimavitvickiy for the initial research in #5485.
---
celery/utils/functional.py | 6 +++---
t/unit/utils/test_functional.py | 1 -
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/celery/utils/functional.py b/celery/utils/functional.py
index d9808d2237f..ddf4c10379d 100644
--- a/celery/utils/functional.py
+++ b/celery/utils/functional.py
@@ -261,11 +261,11 @@ def _argsfromspec(spec, replace_defaults=True):
varargs = spec.varargs
varkw = spec.varkw
if spec.kwonlydefaults:
- split = len(spec.kwonlydefaults)
- kwonlyargs = spec.kwonlyargs[:-split]
+ kwonlyargs = set(spec.kwonlyargs) - set(spec.kwonlydefaults.keys())
if replace_defaults:
kwonlyargs_optional = [
- (kw, i) for i, kw in enumerate(spec.kwonlyargs[-split:])]
+ (kw, i) for i, kw in enumerate(spec.kwonlydefaults.keys())
+ ]
else:
kwonlyargs_optional = list(spec.kwonlydefaults.items())
else:
diff --git a/t/unit/utils/test_functional.py b/t/unit/utils/test_functional.py
index 0fc1f10511a..58ed115b694 100644
--- a/t/unit/utils/test_functional.py
+++ b/t/unit/utils/test_functional.py
@@ -225,7 +225,6 @@ def f(cls, x):
fun = head_from_fun(A.f, bound=True)
assert fun(1) == 1
- @pytest.mark.xfail(reason="Issue #5469")
def test_kwonly_required_args(self):
local = {}
fun = ('def f_kwargs_required(*, a="a", b, c=None):'
From e9cea3e2b5859b5c309426de5a536f3b6fab12d4 Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Thu, 1 Apr 2021 15:31:51 +0600
Subject: [PATCH 193/415] chek if billiard v3.6.4.0 pass tests
---
requirements/default.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/default.txt b/requirements/default.txt
index 33c3b6be9f8..06f249b8c8e 100644
--- a/requirements/default.txt
+++ b/requirements/default.txt
@@ -1,5 +1,5 @@
pytz>dev
-billiard>=3.6.3.0,<4.0
+billiard>=3.6.4.0,<4.0
kombu>=5.0.0,<6.0
vine>=5.0.0,<6.0
click>=7.0,<8.0
From a6bae50be937e3cf0366efac08869b39778646a5 Mon Sep 17 00:00:00 2001
From: Tomas Hrnciar
Date: Thu, 1 Apr 2021 15:40:06 +0200
Subject: [PATCH 194/415] Explicitly require setuptools, bin/celery.py and
utils/imports.py imports pkg_resources
---
requirements/default.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements/default.txt b/requirements/default.txt
index 06f249b8c8e..3fd8529bb1b 100644
--- a/requirements/default.txt
+++ b/requirements/default.txt
@@ -6,3 +6,4 @@ click>=7.0,<8.0
click-didyoumean>=0.0.3
click-repl>=0.1.6
click-plugins>=1.1.1
+setuptools
From 4ab3598a8836a3c2ec6c2ac4aac944df6939de0c Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Thu, 1 Apr 2021 22:35:57 +0600
Subject: [PATCH 195/415] test with kombu v5.1.0b1
---
requirements/default.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/default.txt b/requirements/default.txt
index 3fd8529bb1b..3b7bbe0498f 100644
--- a/requirements/default.txt
+++ b/requirements/default.txt
@@ -1,6 +1,6 @@
pytz>dev
billiard>=3.6.4.0,<4.0
-kombu>=5.0.0,<6.0
+kombu>=5.1.0b1,<6.0
vine>=5.0.0,<6.0
click>=7.0,<8.0
click-didyoumean>=0.0.3
From d7f3e14497d7cc598b82eff6b0eb6430b825f627 Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Fri, 2 Apr 2021 09:03:41 +0600
Subject: [PATCH 196/415] update site link with docs in readme
---
README.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.rst b/README.rst
index 3017bdf04db..d087b1f64e4 100644
--- a/README.rst
+++ b/README.rst
@@ -3,7 +3,7 @@
|build-status| |coverage| |license| |wheel| |pyversion| |pyimp| |ocbackerbadge| |ocsponsorbadge|
:Version: 5.0.5 (singularity)
-:Web: http://celeryproject.org/
+:Web: https://docs.celeryproject.org/en/stable/index.html
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
:Keywords: task, queue, job, async, rabbitmq, amqp, redis,
From beeff198bd26a00dd682a96b6e45419cf4cfbfad Mon Sep 17 00:00:00 2001
From: "Asif Saif Uddin (Auvi)"
Date: Fri, 2 Apr 2021 10:21:15 +0600
Subject: [PATCH 197/415] added changelog for 5.1.0b1
---
Changelog.rst | 27 ++++++++++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/Changelog.rst b/Changelog.rst
index 0bdb9947f8c..c0b6a3191db 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -5,9 +5,34 @@
================
This document contains change notes for bugfix & new features
-in the 5.0.x series, please see :ref:`whatsnew-5.0` for
+in the 5.0.x & 5.1.x series, please see :ref:`whatsnew-5.0` for
an overview of what's new in Celery 5.0.
+.. _version-5.1.0b1:
+
+5.1.0b1
+=======
+:release-date: 2021-04-02 10.15 P.M UTC+6:00
+:release-by: Asif Saif Uddin
+
+- Add sentinel_kwargs to Rendis Sentinel docs.
+- Depend on the maintained python-consul2 library. (#6544).
+- Use result_chord_join_timeout instead of hardcoded default value.
+- Upgrade AzureBlockBlob storage backend to use Azure blob storage library v12 (#6580).
+- Improved integration tests.
+- pass_context for handle_preload_options decorator (#6583).
+- Makes regen less greedy (#6589).
+- Pytest worker shutdown timeout (#6588).
+- Exit celery with non zero exit value if failing (#6602).
+- Raise BackendStoreError when set value is too large for Redis.
+- Trace task optimizations are now set via Celery app instance.
+- Make trace_task_ret and fast_trace_task public.
+- reset_worker_optimizations and create_request_cls has now app as optional parameter.
+- Small refactor in exception handling of on_failure (#6633).
+- Fix for issue #5030 "Celery Result backend on Windows OS".
+- add store_eager_result setting so eager tasks can store result on the result backend (#6614)
+
+
.. _version-5.0.5:
5.0.5
From 9573d39b39d787332a0fd941f918608921a5df68 Mon Sep 17 00:00:00 2001
From: "Asif Saif Uddin (Auvi)"
Date: Fri, 2 Apr 2021 10:49:27 +0600
Subject: [PATCH 198/415] =?UTF-8?q?Bump=20version:=205.0.5=20=E2=86=92=205?=
=?UTF-8?q?.1.0b1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.bumpversion.cfg | 2 +-
README.rst | 8 ++++----
celery/__init__.py | 2 +-
docs/includes/introduction.txt | 15 ++++++---------
4 files changed, 12 insertions(+), 15 deletions(-)
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 0ce811df412..3415054d468 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 5.0.5
+current_version = 5.1.0b1
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z\d]+)?
diff --git a/README.rst b/README.rst
index d087b1f64e4..7fc9498920a 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
|build-status| |coverage| |license| |wheel| |pyversion| |pyimp| |ocbackerbadge| |ocsponsorbadge|
-:Version: 5.0.5 (singularity)
+:Version: 5.1.0b1 (singularity)
:Web: https://docs.celeryproject.org/en/stable/index.html
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
@@ -57,9 +57,9 @@ in such a way that the client enqueues an URL to be requested by a worker.
What do I need?
===============
-Celery version 5.0.5 runs on,
+Celery version 5.1.0b1 runs on,
-- Python (3.6, 3.7, 3.8)
+- Python (3.6, 3.7, 3.8, 3.9)
- PyPy3.6 (7.6)
@@ -89,7 +89,7 @@ Get Started
===========
If this is the first time you're trying to use Celery, or you're
-new to Celery 5.0.5 coming from previous versions then you should read our
+new to Celery 5.0.5 or 5.1.0b1 coming from previous versions then you should read our
getting started tutorials:
- `First steps with Celery`_
diff --git a/celery/__init__.py b/celery/__init__.py
index 33c9902ba08..898c0138add 100644
--- a/celery/__init__.py
+++ b/celery/__init__.py
@@ -17,7 +17,7 @@
SERIES = 'singularity'
-__version__ = '5.0.5'
+__version__ = '5.1.0b1'
__author__ = 'Ask Solem'
__contact__ = 'auvipy@gmail.com'
__homepage__ = 'http://celeryproject.org'
diff --git a/docs/includes/introduction.txt b/docs/includes/introduction.txt
index 11a99ec278b..2f47543eb00 100644
--- a/docs/includes/introduction.txt
+++ b/docs/includes/introduction.txt
@@ -1,4 +1,4 @@
-:Version: 5.0.5 (cliffs)
+:Version: 5.1.0b1 (cliffs)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
@@ -37,16 +37,13 @@ in such a way that the client enqueues an URL to be requested by a worker.
What do I need?
===============
-Celery version 4.4 runs on,
+Celery version 5.1.x runs on,
-- Python (2.7, 3.5, 3.6, 3.7, 38)
-- PyPy2.7 (7.3)
-- PyPy3.5 (7.1)
-- PyPy3.6 (7.3)
+- Python 3.6 or newer versions
+- PyPy3.6 (7.3) or newer
-This is the last version to support Python 2.7,
-and from the next version (Celery 5.x) Python 3.6 or newer is required.
+From the next major version (Celery 6.x) Python 3.7 or newer is required.
If you're running an older version of Python, you need to be running
an older version of Celery:
@@ -71,7 +68,7 @@ Get Started
===========
If this is the first time you're trying to use Celery, or you're
-new to Celery 4.0 coming from previous versions then you should read our
+new to Celery 5.0.x or 5.1.x coming from previous versions then you should read our
getting started tutorials:
- `First steps with Celery`_
From 9be18d177891fa6b43b60af8885e20976961a1a1 Mon Sep 17 00:00:00 2001
From: "Asif Saif Uddin (Auvi)"
Date: Fri, 2 Apr 2021 10:49:52 +0600
Subject: [PATCH 199/415] doc adjustment for 5.1.0b1
---
Changelog.rst | 36 ++++++++++++++++++++++++---
docs/getting-started/introduction.rst | 2 ++
2 files changed, 35 insertions(+), 3 deletions(-)
diff --git a/Changelog.rst b/Changelog.rst
index c0b6a3191db..0f5272c757c 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -6,13 +6,15 @@
This document contains change notes for bugfix & new features
in the 5.0.x & 5.1.x series, please see :ref:`whatsnew-5.0` for
-an overview of what's new in Celery 5.0.
+an overview of what's new in Celery 5.0. 5.1.0b1 is an incremental
+pre release with lots of bug fixes and some new features/enhancements.
+Some dependencies were upgraded to newer versions.
.. _version-5.1.0b1:
5.1.0b1
=======
-:release-date: 2021-04-02 10.15 P.M UTC+6:00
+:release-date: 2021-04-02 10.25 P.M UTC+6:00
:release-by: Asif Saif Uddin
- Add sentinel_kwargs to Rendis Sentinel docs.
@@ -30,7 +32,35 @@ an overview of what's new in Celery 5.0.
- reset_worker_optimizations and create_request_cls has now app as optional parameter.
- Small refactor in exception handling of on_failure (#6633).
- Fix for issue #5030 "Celery Result backend on Windows OS".
-- add store_eager_result setting so eager tasks can store result on the result backend (#6614)
+- Add store_eager_result setting so eager tasks can store result on the result backend (#6614).
+- Allow heartbeats to be sent in tests (#6632).
+- Fixed default visibility timeout note in sqs documentation.
+- Support Redis Sentinel with SSL.
+- Simulate more exhaustive delivery info in apply().
+- Start chord header tasks as soon as possible (#6576).
+- Forward shadow option for retried tasks (#6655).
+--quiet flag now actually makes celery avoid producing logs (#6599).
+- Update platforms.py "superuser privileges" check (#6600).
+- Remove unused property `autoregister` from the Task class (#6624).
+- fnmatch.translate() already translates globs for us. (#6668).
+- Upgrade some syntax to Python 3.6+.
+- Add `azureblockblob_base_path` config (#6669).
+- Fix checking expiration of X.509 certificates (#6678).
+- Drop the lzma extra.
+- Fix JSON decoding errors when using MongoDB as backend (#6675).
+- Allow configuration of RedisBackend's health_check_interval (#6666).
+- Safeguard against schedule entry without kwargs (#6619).
+- Docs only - SQS broker - add STS support (#6693) through kombu.
+- Drop fun_accepts_kwargs backport.
+- Tasks can now have required kwargs at any order (#6699).
+- Min py-amqp 5.0.6.
+- min billiard is now 3.6.4.0.
+- Minimum kombu now is5.1.0b1.
+- Numerous docs fixes.
+- Moved CI to github action.
+- Updated deployment scripts.
+- Updated docker.
+- Initial support of python 3.9 added.
.. _version-5.0.5:
diff --git a/docs/getting-started/introduction.rst b/docs/getting-started/introduction.rst
index f55f448da79..d2ae1d1b261 100644
--- a/docs/getting-started/introduction.rst
+++ b/docs/getting-started/introduction.rst
@@ -46,6 +46,8 @@ What do I need?
Celery 4.x was the last version to support Python 2.7,
Celery 5.x requires Python 3.6 or newer.
+ Celery 5.1.x also requires Python 3.6 or newer.
+
If you're running an older version of Python, you need to be running
an older version of Celery:
From 1509cd8acdef291975ca92f7b9d24ab408d4a4bd Mon Sep 17 00:00:00 2001
From: Awais Qureshi
Date: Sun, 4 Apr 2021 20:11:26 +0500
Subject: [PATCH 200/415] Behavior that used to be called only in Python 2.7
environments without the simplejson package installed is now always used, and
it replaces the original dict subclass with a plain dict after sanity
checking the keys. I think this is a mistake, those code blocks should have
been removed when dropping Python 2.7 support rather than making them the
default behavior. (#6561)
---
celery/app/amqp.py | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/celery/app/amqp.py b/celery/app/amqp.py
index 1a0454e9a92..57429ca00f4 100644
--- a/celery/app/amqp.py
+++ b/celery/app/amqp.py
@@ -316,13 +316,6 @@ def as_task_v2(self, task_id, name, args=None, kwargs=None,
if kwargsrepr is None:
kwargsrepr = saferepr(kwargs, self.kwargsrepr_maxsize)
- if callbacks:
- callbacks = [utf8dict(callback) for callback in callbacks]
- if errbacks:
- errbacks = [utf8dict(errback) for errback in errbacks]
- if chord:
- chord = utf8dict(chord)
-
if not root_id: # empty root_id defaults to task_id
root_id = task_id
@@ -395,13 +388,6 @@ def as_task_v1(self, task_id, name, args=None, kwargs=None,
eta = eta and eta.isoformat()
expires = expires and expires.isoformat()
- if callbacks:
- callbacks = [utf8dict(callback) for callback in callbacks]
- if errbacks:
- errbacks = [utf8dict(errback) for errback in errbacks]
- if chord:
- chord = utf8dict(chord)
-
return task_message(
headers={},
properties={
From be873c7e4eff81f2dd2f7c174a73cfc44ac25fad Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 4 Apr 2021 16:55:40 +0300
Subject: [PATCH 201/415] Update before installing system dependencies.
---
.github/workflows/python-package.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 34fc435ddef..92c652b0913 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -29,7 +29,7 @@ jobs:
steps:
- name: Install apt packages
run: |
- sudo apt-get install -f libcurl4-openssl-dev libssl-dev gnutls-dev httping expect libmemcached-dev
+ sudo apt update && sudo apt-get install -f libcurl4-openssl-dev libssl-dev gnutls-dev httping expect libmemcached-dev
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
From 0953a4d9ecf7008d10236359864d334695cc530c Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 4 Apr 2021 18:26:02 +0300
Subject: [PATCH 202/415] Add support for SQLAlchemy 1.4.
---
celery/backends/database/session.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/celery/backends/database/session.py b/celery/backends/database/session.py
index ca3d683bea6..415d4623e00 100644
--- a/celery/backends/database/session.py
+++ b/celery/backends/database/session.py
@@ -4,12 +4,17 @@
from kombu.utils.compat import register_after_fork
from sqlalchemy import create_engine
from sqlalchemy.exc import DatabaseError
-from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import NullPool
from celery.utils.time import get_exponential_backoff_interval
+try:
+ from sqlalchemy.orm import declarative_base
+except ImportError:
+ # TODO: Remove this once we drop support for SQLAlchemy < 1.4.
+ from sqlalchemy.ext.declarative import declarative_base
+
ResultModelBase = declarative_base()
__all__ = ('SessionManager',)
From 4f2213a427861cf42b778ef499f29b179d8c40ed Mon Sep 17 00:00:00 2001
From: Sardorbek Imomaliev
Date: Tue, 6 Apr 2021 21:10:50 +0700
Subject: [PATCH 203/415] Fix typo in Changelog.rst
---
Changelog.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Changelog.rst b/Changelog.rst
index 0f5272c757c..0cb7e6a6c5e 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -17,7 +17,7 @@ Some dependencies were upgraded to newer versions.
:release-date: 2021-04-02 10.25 P.M UTC+6:00
:release-by: Asif Saif Uddin
-- Add sentinel_kwargs to Rendis Sentinel docs.
+- Add sentinel_kwargs to Redis Sentinel docs.
- Depend on the maintained python-consul2 library. (#6544).
- Use result_chord_join_timeout instead of hardcoded default value.
- Upgrade AzureBlockBlob storage backend to use Azure blob storage library v12 (#6580).
From 1901ea8594185c015d1518d89f3b90180275c0b9 Mon Sep 17 00:00:00 2001
From: "Stephen J. Fuhry"
Date: Sat, 10 Apr 2021 15:31:20 +0000
Subject: [PATCH 204/415] fix AttributeError regression in #6619
---
celery/beat.py | 2 +-
t/unit/app/test_beat.py | 17 +++++++++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/celery/beat.py b/celery/beat.py
index 2b251e838a2..74c67f94ed9 100644
--- a/celery/beat.py
+++ b/celery/beat.py
@@ -208,7 +208,7 @@ def _evaluate_entry_args(entry_args):
return []
return [
v() if isinstance(v, BeatLazyFunc) else v
- for v in entry_args.args
+ for v in entry_args
]
diff --git a/t/unit/app/test_beat.py b/t/unit/app/test_beat.py
index 98f4f21bf3f..739a45e5e24 100644
--- a/t/unit/app/test_beat.py
+++ b/t/unit/app/test_beat.py
@@ -212,6 +212,23 @@ def foo():
scheduler.apply_async(entry, advance=False)
foo.apply_async.assert_called()
+ def test_apply_async_without_null_args(self):
+
+ @self.app.task(shared=False)
+ def foo(moo: int):
+ return moo
+ foo.apply_async = Mock(name='foo.apply_async')
+
+ scheduler = mScheduler(app=self.app)
+ entry = scheduler.Entry(task=foo.name, app=self.app, args=None,
+ kwargs=None)
+ entry.args = (101,)
+ entry.kwargs = None
+
+ scheduler.apply_async(entry, advance=False)
+ foo.apply_async.assert_called()
+ assert foo.apply_async.call_args[0][0] == [101]
+
def test_should_sync(self):
@self.app.task(shared=False)
From 81df81acf8605ba3802810c7901be7d905c5200b Mon Sep 17 00:00:00 2001
From: Joel Payne <15524072+LilSpazJoekp@users.noreply.github.com>
Date: Mon, 12 Apr 2021 11:04:24 -0500
Subject: [PATCH 205/415] Fix tiny typo
---
Changelog.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Changelog.rst b/Changelog.rst
index 0cb7e6a6c5e..cafe66d43ad 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -39,7 +39,7 @@ Some dependencies were upgraded to newer versions.
- Simulate more exhaustive delivery info in apply().
- Start chord header tasks as soon as possible (#6576).
- Forward shadow option for retried tasks (#6655).
---quiet flag now actually makes celery avoid producing logs (#6599).
+- --quiet flag now actually makes celery avoid producing logs (#6599).
- Update platforms.py "superuser privileges" check (#6600).
- Remove unused property `autoregister` from the Task class (#6624).
- fnmatch.translate() already translates globs for us. (#6668).
From 8ebcce1523d79039f23da748f00bec465951de2a Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 18 Apr 2021 16:01:24 +0300
Subject: [PATCH 206/415] `some_task.apply_async(ignore_result=True)` now
avoids persisting the result (#6713)
* Add a test case which proves that the result is persisted when ignore_result is passed through apply_async.
* Add unit test for Request update
* Update Request class to include ignore_result
* Send ignore_result in AMQP message
Update mark_done to check request.ignore_result
* Remove xfail mark
* Fix request None case
* Add ignore_result documentation
* Add ignore_result to apply
* Remove file created by tests
Co-authored-by: Josue Balandrano Coronel
---
celery/app/amqp.py | 5 ++--
celery/app/base.py | 7 ++---
celery/app/task.py | 51 ++++++++++++++++++++---------------
celery/backends/base.py | 8 +++++-
celery/worker/request.py | 5 ++++
t/integration/test_tasks.py | 4 +++
t/unit/worker/test_request.py | 33 ++++++++++++++++-------
7 files changed, 76 insertions(+), 37 deletions(-)
diff --git a/celery/app/amqp.py b/celery/app/amqp.py
index 57429ca00f4..a574b2dd792 100644
--- a/celery/app/amqp.py
+++ b/celery/app/amqp.py
@@ -284,7 +284,7 @@ def as_task_v2(self, task_id, name, args=None, kwargs=None,
time_limit=None, soft_time_limit=None,
create_sent_event=False, root_id=None, parent_id=None,
shadow=None, chain=None, now=None, timezone=None,
- origin=None, argsrepr=None, kwargsrepr=None):
+ origin=None, ignore_result=False, argsrepr=None, kwargsrepr=None):
args = args or ()
kwargs = kwargs or {}
if not isinstance(args, (list, tuple)):
@@ -335,7 +335,8 @@ def as_task_v2(self, task_id, name, args=None, kwargs=None,
'parent_id': parent_id,
'argsrepr': argsrepr,
'kwargsrepr': kwargsrepr,
- 'origin': origin or anon_nodename()
+ 'origin': origin or anon_nodename(),
+ 'ignore_result': ignore_result,
},
properties={
'correlation_id': task_id,
diff --git a/celery/app/base.py b/celery/app/base.py
index d833fc1e0e6..5163168d23b 100644
--- a/celery/app/base.py
+++ b/celery/app/base.py
@@ -716,7 +716,7 @@ def send_task(self, name, args=None, kwargs=None, countdown=None,
'task_always_eager has no effect on send_task',
), stacklevel=2)
- ignored_result = options.pop('ignore_result', False)
+ ignore_result = options.pop('ignore_result', False)
options = router.route(
options, route_name or name, args, kwargs, task_type)
@@ -739,6 +739,7 @@ def send_task(self, name, args=None, kwargs=None, countdown=None,
reply_to or self.thread_oid, time_limit, soft_time_limit,
self.conf.task_send_sent_event,
root_id, parent_id, shadow, chain,
+ ignore_result=ignore_result,
argsrepr=options.get('argsrepr'),
kwargsrepr=options.get('kwargsrepr'),
)
@@ -748,14 +749,14 @@ def send_task(self, name, args=None, kwargs=None, countdown=None,
with self.producer_or_acquire(producer) as P:
with P.connection._reraise_as_library_errors():
- if not ignored_result:
+ if not ignore_result:
self.backend.on_task_call(P, task_id)
amqp.send_task_message(P, name, message, **options)
result = (result_cls or self.AsyncResult)(task_id)
# We avoid using the constructor since a custom result class
# can be used, in which case the constructor may still use
# the old signature.
- result.ignored = ignored_result
+ result.ignored = ignore_result
if add_to_parent:
if not have_parent:
diff --git a/celery/app/task.py b/celery/app/task.py
index 53dd79b21fc..cb24e55589f 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -61,36 +61,37 @@ def _reprtask(task, fmt=None, flags=None):
class Context:
"""Task request variables (Task.request)."""
- logfile = None
- loglevel = None
- hostname = None
- id = None
+ _children = None # see property
+ _protected = 0
args = None
- kwargs = None
- retries = 0
+ callbacks = None
+ called_directly = True
+ chain = None
+ chord = None
+ correlation_id = None
+ delivery_info = None
+ errbacks = None
eta = None
expires = None
- is_eager = False
+ group = None
+ group_index = None
headers = None
- delivery_info = None
+ hostname = None
+ id = None
+ ignore_result = False
+ is_eager = False
+ kwargs = None
+ logfile = None
+ loglevel = None
+ origin = None
+ parent_id = None
+ retries = 0
reply_to = None
- shadow = None
root_id = None
- parent_id = None
- correlation_id = None
+ shadow = None
taskset = None # compat alias to group
- group = None
- group_index = None
- chord = None
- chain = None
- utc = None
- called_directly = True
- callbacks = None
- errbacks = None
timelimit = None
- origin = None
- _children = None # see property
- _protected = 0
+ utc = None
def __init__(self, *args, **kwargs):
self.update(*args, **kwargs)
@@ -504,6 +505,11 @@ def apply_async(self, args=None, kwargs=None, task_id=None, producer=None,
attribute. Trailing can also be disabled by default using the
:attr:`trail` attribute
+ ignore_result (bool): If set to `False` (default) the result
+ of a task will be stored in the backend. If set to `True`
+ the result will not be stored. This can also be set
+ using the :attr:`ignore_result` in the `app.task` decorator.
+
publisher (kombu.Producer): Deprecated alias to ``producer``.
headers (Dict): Message headers to be included in the message.
@@ -768,6 +774,7 @@ def apply(self, args=None, kwargs=None,
'callbacks': maybe_list(link),
'errbacks': maybe_list(link_error),
'headers': headers,
+ 'ignore_result': options.get('ignore_result', False),
'delivery_info': {
'is_eager': True,
'exchange': options.get('exchange'),
diff --git a/celery/backends/base.py b/celery/backends/base.py
index 7c5dcfa357c..fdec6d58f46 100644
--- a/celery/backends/base.py
+++ b/celery/backends/base.py
@@ -76,6 +76,12 @@ def ignore(self, *a, **kw):
__setitem__ = update = setdefault = ignore
+def _is_request_ignore_result(request):
+ if request is None:
+ return False
+ return request.ignore_result
+
+
class Backend:
READY_STATES = states.READY_STATES
UNREADY_STATES = states.UNREADY_STATES
@@ -150,7 +156,7 @@ def mark_as_started(self, task_id, **meta):
def mark_as_done(self, task_id, result,
request=None, store_result=True, state=states.SUCCESS):
"""Mark task as successfully executed."""
- if store_result:
+ if (store_result and not _is_request_ignore_result(request)):
self.store_result(task_id, result, state, request=request)
if request and request.chord:
self.on_chord_part_return(request, state, result)
diff --git a/celery/worker/request.py b/celery/worker/request.py
index c1847820aae..832c6f379ba 100644
--- a/celery/worker/request.py
+++ b/celery/worker/request.py
@@ -120,6 +120,7 @@ def __init__(self, message, on_ack=noop,
self._eventer = eventer
self._connection_errors = connection_errors or ()
self._task = task or self._app.tasks[self._type]
+ self._ignore_result = self._request_dict.get('ignore_result', False)
# timezone means the message is timezone-aware, and the only timezone
# supported at this point is UTC.
@@ -240,6 +241,10 @@ def on_reject(self, value):
def hostname(self):
return self._hostname
+ @property
+ def ignore_result(self):
+ return self._ignore_result
+
@property
def eventer(self):
return self._eventer
diff --git a/t/integration/test_tasks.py b/t/integration/test_tasks.py
index 17d59f9851d..c7c41214e54 100644
--- a/t/integration/test_tasks.py
+++ b/t/integration/test_tasks.py
@@ -100,6 +100,10 @@ def test_ignore_result(self, manager):
"""Testing calling task with ignoring results."""
result = add.apply_async((1, 2), ignore_result=True)
assert result.get() is None
+ # We wait since it takes a bit of time for the result to be
+ # persisted in the result backend.
+ sleep(1)
+ assert result.result is None
@flaky
def test_timeout(self, manager):
diff --git a/t/unit/worker/test_request.py b/t/unit/worker/test_request.py
index 013cdf01aea..d8f7de6ad1d 100644
--- a/t/unit/worker/test_request.py
+++ b/t/unit/worker/test_request.py
@@ -89,8 +89,9 @@ def mro(cls):
assert mro_lookup(D, 'x') is None
-def jail(app, task_id, name, args, kwargs):
+def jail(app, task_id, name, request_opts, args, kwargs):
request = {'id': task_id}
+ request.update(request_opts)
task = app.tasks[name]
task.__trace__ = None # rebuild
return trace_task(
@@ -115,7 +116,7 @@ def test_process_cleanup_fails(self, patching):
self.mytask.backend = Mock()
self.mytask.backend.process_cleanup = Mock(side_effect=KeyError())
tid = uuid()
- ret = jail(self.app, tid, self.mytask.name, [2], {})
+ ret = jail(self.app, tid, self.mytask.name, {}, [2], {})
assert ret == 4
self.mytask.backend.mark_as_done.assert_called()
assert 'Process cleanup failed' in _logger.error.call_args[0][0]
@@ -124,10 +125,10 @@ def test_process_cleanup_BaseException(self):
self.mytask.backend = Mock()
self.mytask.backend.process_cleanup = Mock(side_effect=SystemExit())
with pytest.raises(SystemExit):
- jail(self.app, uuid(), self.mytask.name, [2], {})
+ jail(self.app, uuid(), self.mytask.name, {}, [2], {})
def test_execute_jail_success(self):
- ret = jail(self.app, uuid(), self.mytask.name, [2], {})
+ ret = jail(self.app, uuid(), self.mytask.name, {}, [2], {})
assert ret == 4
def test_marked_as_started(self):
@@ -141,29 +142,43 @@ def store_result(tid, meta, state, **kwargs):
self.mytask.track_started = True
tid = uuid()
- jail(self.app, tid, self.mytask.name, [2], {})
+ jail(self.app, tid, self.mytask.name, {}, [2], {})
assert tid in _started
self.mytask.ignore_result = True
tid = uuid()
- jail(self.app, tid, self.mytask.name, [2], {})
+ jail(self.app, tid, self.mytask.name, {}, [2], {})
assert tid not in _started
def test_execute_jail_failure(self):
ret = jail(
- self.app, uuid(), self.mytask_raising.name, [4], {},
+ self.app, uuid(), self.mytask_raising.name, {}, [4], {},
)
assert isinstance(ret, ExceptionInfo)
assert ret.exception.args == (4,)
- def test_execute_ignore_result(self):
+ def test_execute_task_ignore_result(self):
@self.app.task(shared=False, ignore_result=True)
def ignores_result(i):
return i ** i
task_id = uuid()
- ret = jail(self.app, task_id, ignores_result.name, [4], {})
+ ret = jail(self.app, task_id, ignores_result.name, {}, [4], {})
+ assert ret == 256
+ assert not self.app.AsyncResult(task_id).ready()
+
+ def test_execute_request_ignore_result(self):
+
+ @self.app.task(shared=False)
+ def ignores_result(i):
+ return i ** i
+
+ task_id = uuid()
+ ret = jail(
+ self.app, task_id, ignores_result.name,
+ {'ignore_result': True}, [4], {}
+ )
assert ret == 256
assert not self.app.AsyncResult(task_id).ready()
From 25f6c139e11edd32f5c36542c737ee7c7de2e9cc Mon Sep 17 00:00:00 2001
From: Maarten Fonville
Date: Thu, 22 Apr 2021 17:09:56 +0200
Subject: [PATCH 207/415] Update systemd tmpfiles path (#6688)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
To fix `/etc/tmpfiles.d/celery.conf:1: Line references path below legacy directory /var/run/, updating /var/run/celery → /run/celery; please update the tmpfiles.d/ drop-in file accordingly`
---
docs/userguide/daemonizing.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/userguide/daemonizing.rst b/docs/userguide/daemonizing.rst
index 8b74f73bfb4..cd46c4e1894 100644
--- a/docs/userguide/daemonizing.rst
+++ b/docs/userguide/daemonizing.rst
@@ -432,7 +432,7 @@ You can also use systemd-tmpfiles in order to create working directories (for lo
.. code-block:: bash
- d /var/run/celery 0755 celery celery -
+ d /run/celery 0755 celery celery -
d /var/log/celery 0755 celery celery -
From 5a908b2c128b968da88cd7daf6e195acddc4f295 Mon Sep 17 00:00:00 2001
From: Geunsik Lim
Date: Sat, 24 Apr 2021 17:11:40 +0900
Subject: [PATCH 208/415] Fixed incorrect coding style (textwidth) in ./app
Fixed issue #6739.
This commit is trivial. It is to fix incorrect coding style
(text width) that is applied into the ./app/ folder.
* https://github.com/celery/celery/blob/master/CONTRIBUTING.rst#id79
* soft limit: 78
* hard limit: 79
Signed-off-by: Geunsik Lim
Signed-off-by: Geunsik Lim
---
celery/app/base.py | 3 ++-
celery/app/task.py | 3 ++-
celery/app/utils.py | 3 ++-
3 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/celery/app/base.py b/celery/app/base.py
index 5163168d23b..27f1d90f779 100644
--- a/celery/app/base.py
+++ b/celery/app/base.py
@@ -275,7 +275,8 @@ def __init__(self, main=None, loader=None, backend=None,
self.__autoset('result_backend', backend)
self.__autoset('include', include)
self.__autoset('broker_use_ssl', kwargs.get('broker_use_ssl'))
- self.__autoset('redis_backend_use_ssl', kwargs.get('redis_backend_use_ssl'))
+ self.__autoset('redis_backend_use_ssl',
+ kwargs.get('redis_backend_use_ssl'))
self._conf = Settings(
PendingConfiguration(
self._preconf, self._finalize_pending_conf),
diff --git a/celery/app/task.py b/celery/app/task.py
index cb24e55589f..3e8461b6b11 100644
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -969,7 +969,8 @@ def update_state(self, task_id=None, state=None, meta=None, **kwargs):
"""
if task_id is None:
task_id = self.request.id
- self.backend.store_result(task_id, meta, state, request=self.request, **kwargs)
+ self.backend.store_result(
+ task_id, meta, state, request=self.request, **kwargs)
def on_success(self, retval, task_id, args, kwargs):
"""Success handler.
diff --git a/celery/app/utils.py b/celery/app/utils.py
index 05aeb1e5016..8b72652e708 100644
--- a/celery/app/utils.py
+++ b/celery/app/utils.py
@@ -394,7 +394,8 @@ def find_app(app, symbol_by_name=symbol_by_name, imp=import_from_cwd):
try:
found = sym.celery
if isinstance(found, ModuleType):
- raise AttributeError("attribute 'celery' is the celery module not the instance of celery")
+ raise AttributeError(
+ "attribute 'celery' is the celery module not the instance of celery")
except AttributeError:
if getattr(sym, '__path__', None):
try:
From 27c9b7796d99c0d3d8ff030553f2e37132fed5a3 Mon Sep 17 00:00:00 2001
From: aruseni
Date: Sat, 24 Apr 2021 16:49:02 +0300
Subject: [PATCH 209/415] Minor changes to the django/proj/celery.py example
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
See #6738 for an explanation why it’s better to say “registered apps”.
---
examples/django/proj/celery.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/examples/django/proj/celery.py b/examples/django/proj/celery.py
index 429afff312a..9766a2ac2ee 100644
--- a/examples/django/proj/celery.py
+++ b/examples/django/proj/celery.py
@@ -2,7 +2,7 @@
from celery import Celery
-# set the default Django settings module for the 'celery' program.
+# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings')
app = Celery('proj')
@@ -13,7 +13,7 @@
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
-# Load task modules from all registered Django app configs.
+# Load task modules from all registered Django apps.
app.autodiscover_tasks()
From 850c62a67d41430058e60f5904aaff77fe3cd626 Mon Sep 17 00:00:00 2001
From: Parth Joshi
Date: Sun, 25 Apr 2021 18:21:16 +0530
Subject: [PATCH 210/415] Ensure AMQPContext exposes an app attribute (#6741)
`handle_preload_options` introduced in celery v5.0.3 expects the context object it receives to have an app attribute. In case of the `celery amqp` command the CLIContext object gets wrapped around by an AMQPContext which does not expose this attribute. This tiny modification fixes that by making AMQPContext expose an app by delegating to the underlying CLIContext object.
---
celery/bin/amqp.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/celery/bin/amqp.py b/celery/bin/amqp.py
index ab8ab5f0100..29c625281ed 100644
--- a/celery/bin/amqp.py
+++ b/celery/bin/amqp.py
@@ -25,6 +25,10 @@ def __init__(self, cli_context):
self.connection = self.cli_context.app.connection()
self.channel = None
self.reconnect()
+
+ @property
+ def app(self):
+ return self.cli_context.app
def respond(self, retval):
if isinstance(retval, str):
From 230c9acd951dddad0a73ddc5b735f630acdfc12a Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 5 Apr 2021 15:42:52 +0300
Subject: [PATCH 211/415] Inspect commands now accept arguments.
Fixes #6705.
---
celery/bin/control.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/celery/bin/control.py b/celery/bin/control.py
index 507c5ec8efb..a13963a54b3 100644
--- a/celery/bin/control.py
+++ b/celery/bin/control.py
@@ -95,7 +95,8 @@ def status(ctx, timeout, destination, json, **kwargs):
nodecount, text.pluralize(nodecount, 'node')))
-@click.command(cls=CeleryCommand)
+@click.command(cls=CeleryCommand,
+ context_settings={'allow_extra_args': True})
@click.argument("action", type=click.Choice([
name for name, info in Panel.meta.items()
if info.type == 'inspect' and info.visible
@@ -128,9 +129,12 @@ def inspect(ctx, action, timeout, destination, json, **kwargs):
"""
callback = None if json else partial(_say_remote_command_reply, ctx,
show_reply=True)
- replies = ctx.obj.app.control.inspect(timeout=timeout,
+ arguments = _compile_arguments(action, ctx.args)
+ inspect = ctx.obj.app.control.inspect(timeout=timeout,
destination=destination,
- callback=callback)._request(action)
+ callback=callback)
+ replies = inspect._request(action,
+ **arguments)
if not replies:
raise CeleryCommandException(
From ce8a9036c110ce7c70cf3c0bc9d4f2185916f585 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Wed, 28 Apr 2021 19:43:41 +1000
Subject: [PATCH 212/415] fix: Chord counting of group children (#6733)
* improv: Deconflict `chord` class and kwarg names
* improv: Make `chord.descend` protected not private
This will allow us to call it from other code in this module which needs
to accurately count chord sizes.
* fix: Counting of chord-chain tails of zero tasks
* fix: Chord counting of group children
This change ensures that we only have one piece of code which calculates
chord sizes (ie. `_chord._descend()`, recently made protected so other
canvas classes can use it as required). By doing so, we fix some edge
cases in the chord counting logic which was being used for children of
groups, and also add some unit tests to capture those cases and their
expected behaviours.
This change also introduces an integration test which checks the current
behaviour of chains used as chord bodies when nested in groups. Due to
some misbehaviour, likely with promise fulfillment, the `GroupResult`
object will time out unless all of its children are resolved prior to
`GroupResult` being joined (specifically, native joins block forever or
until timeout). This misbehaviour is tracked by #6734 and the test in
not marked as `xfail`ing to ensure that the current janky behaviour
continues to work as expected rather than regressing.
---
celery/canvas.py | 50 +++++----
t/integration/test_canvas.py | 106 +++++++++++++++++++
t/unit/tasks/test_canvas.py | 190 ++++++++++++++++++++++++++++++++++-
3 files changed, 328 insertions(+), 18 deletions(-)
diff --git a/celery/canvas.py b/celery/canvas.py
index 57b0aea0628..a80e979af96 100644
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -1170,21 +1170,25 @@ def _apply_tasks(self, tasks, producer=None, app=None, p=None,
# we are able to tell when we are at the end by checking if
# next_task is None. This enables us to set the chord size
# without burning through the entire generator. See #3021.
+ chord_size = 0
for task_index, (current_task, next_task) in enumerate(
lookahead(tasks)
):
+ # We expect that each task must be part of the same group which
+ # seems sensible enough. If that's somehow not the case we'll
+ # end up messing up chord counts and there are all sorts of
+ # awful race conditions to think about. We'll hope it's not!
sig, res, group_id = current_task
- _chord = sig.options.get("chord") or chord
- if _chord is not None and next_task is None:
- chord_size = task_index + 1
- if isinstance(sig, _chain):
- if sig.tasks[-1].subtask_type == 'chord':
- chord_size = sig.tasks[-1].__length_hint__()
- else:
- chord_size = task_index + len(sig.tasks[-1])
+ chord_obj = sig.options.get("chord") or chord
+ # We need to check the chord size of each contributing task so
+ # that when we get to the final one, we can correctly set the
+ # size in the backend and the chord can be sensible completed.
+ chord_size += _chord._descend(sig)
+ if chord_obj is not None and next_task is None:
+ # Per above, sanity check that we only saw one group
app.backend.set_chord_size(group_id, chord_size)
sig.apply_async(producer=producer, add_to_parent=False,
- chord=_chord, args=args, kwargs=kwargs,
+ chord=chord_obj, args=args, kwargs=kwargs,
**options)
# adding callback to result, such that it will gradually
# fulfill the barrier.
@@ -1296,8 +1300,8 @@ def app(self):
return app if app is not None else current_app
-@Signature.register_type()
-class chord(Signature):
+@Signature.register_type(name="chord")
+class _chord(Signature):
r"""Barrier synchronization primitive.
A chord consists of a header and a body.
@@ -1415,20 +1419,27 @@ def apply(self, args=None, kwargs=None,
)
@classmethod
- def __descend(cls, sig_obj):
+ def _descend(cls, sig_obj):
# Sometimes serialized signatures might make their way here
if not isinstance(sig_obj, Signature) and isinstance(sig_obj, dict):
sig_obj = Signature.from_dict(sig_obj)
if isinstance(sig_obj, group):
# Each task in a group counts toward this chord
subtasks = getattr(sig_obj.tasks, "tasks", sig_obj.tasks)
- return sum(cls.__descend(task) for task in subtasks)
+ return sum(cls._descend(task) for task in subtasks)
elif isinstance(sig_obj, _chain):
- # The last element in a chain counts toward this chord
- return cls.__descend(sig_obj.tasks[-1])
+ # The last non-empty element in a chain counts toward this chord
+ for child_sig in sig_obj.tasks[-1::-1]:
+ child_size = cls._descend(child_sig)
+ if child_size > 0:
+ return child_size
+ else:
+ # We have to just hope this chain is part of some encapsulating
+ # signature which is valid and can fire the chord body
+ return 0
elif isinstance(sig_obj, chord):
# The child chord's body counts toward this chord
- return cls.__descend(sig_obj.body)
+ return cls._descend(sig_obj.body)
elif isinstance(sig_obj, Signature):
# Each simple signature counts as 1 completion for this chord
return 1
@@ -1437,7 +1448,7 @@ def __descend(cls, sig_obj):
def __length_hint__(self):
tasks = getattr(self.tasks, "tasks", self.tasks)
- return sum(self.__descend(task) for task in tasks)
+ return sum(self._descend(task) for task in tasks)
def run(self, header, body, partial_args, app=None, interval=None,
countdown=1, max_retries=None, eager=False,
@@ -1537,6 +1548,11 @@ def _get_app(self, body=None):
body = getitem_property('kwargs.body', 'Body task of chord.')
+# Add a back-compat alias for the previous `chord` class name which conflicts
+# with keyword arguments elsewhere in this file
+chord = _chord
+
+
def signature(varies, *args, **kwargs):
"""Create new signature.
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 4c5f31a495f..28560e33e64 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -704,6 +704,112 @@ def test_nested_group_group(self, manager):
res = sig.delay()
assert res.get(timeout=TIMEOUT) == [42, 42]
+ def test_nested_group_chord_counting_simple(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ gchild_sig = identity.si(42)
+ child_chord = chord((gchild_sig, ), identity.s())
+ group_sig = group((child_chord, ))
+ res = group_sig.delay()
+ # Wait for the result to land and confirm its value is as expected
+ assert res.get(timeout=TIMEOUT) == [[42]]
+
+ def test_nested_group_chord_counting_chain(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ gchild_count = 42
+ gchild_sig = chain((identity.si(1337), ) * gchild_count)
+ child_chord = chord((gchild_sig, ), identity.s())
+ group_sig = group((child_chord, ))
+ res = group_sig.delay()
+ # Wait for the result to land and confirm its value is as expected
+ assert res.get(timeout=TIMEOUT) == [[1337]]
+
+ def test_nested_group_chord_counting_group(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ gchild_count = 42
+ gchild_sig = group((identity.si(1337), ) * gchild_count)
+ child_chord = chord((gchild_sig, ), identity.s())
+ group_sig = group((child_chord, ))
+ res = group_sig.delay()
+ # Wait for the result to land and confirm its value is as expected
+ assert res.get(timeout=TIMEOUT) == [[1337] * gchild_count]
+
+ def test_nested_group_chord_counting_chord(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ gchild_count = 42
+ gchild_sig = chord(
+ (identity.si(1337), ) * gchild_count, identity.si(31337),
+ )
+ child_chord = chord((gchild_sig, ), identity.s())
+ group_sig = group((child_chord, ))
+ res = group_sig.delay()
+ # Wait for the result to land and confirm its value is as expected
+ assert res.get(timeout=TIMEOUT) == [[31337]]
+
+ def test_nested_group_chord_counting_mixed(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ gchild_count = 42
+ child_chord = chord(
+ (
+ identity.si(42),
+ chain((identity.si(42), ) * gchild_count),
+ group((identity.si(42), ) * gchild_count),
+ chord((identity.si(42), ) * gchild_count, identity.si(1337)),
+ ),
+ identity.s(),
+ )
+ group_sig = group((child_chord, ))
+ res = group_sig.delay()
+ # Wait for the result to land and confirm its value is as expected. The
+ # group result gets unrolled into the encapsulating chord, hence the
+ # weird unpacking below
+ assert res.get(timeout=TIMEOUT) == [
+ [42, 42, *((42, ) * gchild_count), 1337]
+ ]
+
+ @pytest.mark.xfail(raises=TimeoutError, reason="#6734")
+ def test_nested_group_chord_body_chain(self, manager):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ child_chord = chord(identity.si(42), chain((identity.s(), )))
+ group_sig = group((child_chord, ))
+ res = group_sig.delay()
+ # The result can be expected to timeout since it seems like its
+ # underlying promise might not be getting fulfilled (ref #6734). Pick a
+ # short timeout since we don't want to block for ages and this is a
+ # fairly simple signature which should run pretty quickly.
+ expected_result = [[42]]
+ with pytest.raises(TimeoutError) as expected_excinfo:
+ res.get(timeout=TIMEOUT / 10)
+ # Get the child `AsyncResult` manually so that we don't have to wait
+ # again for the `GroupResult`
+ assert res.children[0].get(timeout=TIMEOUT) == expected_result[0]
+ assert res.get(timeout=TIMEOUT) == expected_result
+ # Re-raise the expected exception so this test will XFAIL
+ raise expected_excinfo.value
+
def assert_ids(r, expected_value, expected_root_id, expected_parent_id):
root_id, parent_id, value = r.get(timeout=TIMEOUT)
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
index 6f638d04262..c6e9ca86035 100644
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -1,5 +1,5 @@
import json
-from unittest.mock import MagicMock, Mock, call, patch, sentinel
+from unittest.mock import MagicMock, Mock, call, patch, sentinel, ANY
import pytest
import pytest_subtests # noqa: F401
@@ -782,6 +782,194 @@ def test_kwargs_delay_partial(self):
res = self.helper_test_get_delay(x.delay(y=1))
assert res == [2, 2]
+ def test_apply_from_generator(self):
+ child_count = 42
+ child_sig = self.add.si(0, 0)
+ child_sigs_gen = (child_sig for _ in range(child_count))
+ group_sig = group(child_sigs_gen)
+ with patch("celery.canvas.Signature.apply_async") as mock_apply_async:
+ res_obj = group_sig.apply_async()
+ assert mock_apply_async.call_count == child_count
+ assert len(res_obj.children) == child_count
+
+ # This needs the current app for some reason not worth digging into
+ @pytest.mark.usefixtures('depends_on_current_app')
+ def test_apply_from_generator_empty(self):
+ empty_gen = (False for _ in range(0))
+ group_sig = group(empty_gen)
+ with patch("celery.canvas.Signature.apply_async") as mock_apply_async:
+ res_obj = group_sig.apply_async()
+ assert mock_apply_async.call_count == 0
+ assert len(res_obj.children) == 0
+
+ # In the following tests, getting the group ID is a pain so we just use
+ # `ANY` to wildcard it when we're checking on calls made to our mocks
+ def test_apply_contains_chord(self):
+ gchild_count = 42
+ gchild_sig = self.add.si(0, 0)
+ gchild_sigs = (gchild_sig, ) * gchild_count
+ child_chord = chord(gchild_sigs, gchild_sig)
+ group_sig = group((child_chord, ))
+ with patch.object(
+ self.app.backend, "set_chord_size",
+ ) as mock_set_chord_size, patch(
+ "celery.canvas.Signature.apply_async",
+ ) as mock_apply_async:
+ res_obj = group_sig.apply_async()
+ # We only see applies for the header grandchildren because the tasks
+ # are never actually run due to our mocking of `apply_async()`
+ assert mock_apply_async.call_count == gchild_count
+ assert len(res_obj.children) == len(group_sig.tasks)
+ # We must have set the chord size for the group of tasks which makes up
+ # the header of the `child_chord`, just before we apply the last task.
+ mock_set_chord_size.assert_called_once_with(ANY, gchild_count)
+
+ def test_apply_contains_chords_containing_chain(self):
+ ggchild_count = 42
+ ggchild_sig = self.add.si(0, 0)
+ gchild_sig = chain((ggchild_sig, ) * ggchild_count)
+ child_count = 24
+ child_chord = chord((gchild_sig, ), ggchild_sig)
+ group_sig = group((child_chord, ) * child_count)
+ with patch.object(
+ self.app.backend, "set_chord_size",
+ ) as mock_set_chord_size, patch(
+ "celery.canvas.Signature.apply_async",
+ ) as mock_apply_async:
+ res_obj = group_sig.apply_async()
+ # We only see applies for the header grandchildren because the tasks
+ # are never actually run due to our mocking of `apply_async()`
+ assert mock_apply_async.call_count == child_count
+ assert len(res_obj.children) == child_count
+ # We must have set the chord sizes based on the number of tail tasks of
+ # the encapsulated chains - in this case 1 for each child chord
+ mock_set_chord_size.assert_has_calls((call(ANY, 1), ) * child_count)
+
+ @pytest.mark.xfail(reason="Invalid canvas setup with bad exception")
+ def test_apply_contains_chords_containing_empty_chain(self):
+ gchild_sig = chain(tuple())
+ child_count = 24
+ child_chord = chord((gchild_sig, ), self.add.si(0, 0))
+ group_sig = group((child_chord, ) * child_count)
+ # This is an invalid setup because we can't complete a chord header if
+ # there are no actual tasks which will run in it. However, the current
+ # behaviour of an `IndexError` isn't particularly helpful to a user.
+ res_obj = group_sig.apply_async()
+
+ def test_apply_contains_chords_containing_chain_with_empty_tail(self):
+ ggchild_count = 42
+ ggchild_sig = self.add.si(0, 0)
+ tail_count = 24
+ gchild_sig = chain(
+ (ggchild_sig, ) * ggchild_count +
+ (group((ggchild_sig, ) * tail_count), group(tuple()), ),
+ )
+ child_chord = chord((gchild_sig, ), ggchild_sig)
+ group_sig = group((child_chord, ))
+ with patch.object(
+ self.app.backend, "set_chord_size",
+ ) as mock_set_chord_size, patch(
+ "celery.canvas.Signature.apply_async",
+ ) as mock_apply_async:
+ res_obj = group_sig.apply_async()
+ # We only see applies for the header grandchildren because the tasks
+ # are never actually run due to our mocking of `apply_async()`
+ assert mock_apply_async.call_count == 1
+ assert len(res_obj.children) == 1
+ # We must have set the chord sizes based on the size of the last
+ # non-empty task in the encapsulated chains - in this case `tail_count`
+ # for the group preceding the empty one in each grandchild chain
+ mock_set_chord_size.assert_called_once_with(ANY, tail_count)
+
+ def test_apply_contains_chords_containing_group(self):
+ ggchild_count = 42
+ ggchild_sig = self.add.si(0, 0)
+ gchild_sig = group((ggchild_sig, ) * ggchild_count)
+ child_count = 24
+ child_chord = chord((gchild_sig, ), ggchild_sig)
+ group_sig = group((child_chord, ) * child_count)
+ with patch.object(
+ self.app.backend, "set_chord_size",
+ ) as mock_set_chord_size, patch(
+ "celery.canvas.Signature.apply_async",
+ ) as mock_apply_async:
+ res_obj = group_sig.apply_async()
+ # We see applies for all of the header grandchildren because the tasks
+ # are never actually run due to our mocking of `apply_async()`
+ assert mock_apply_async.call_count == child_count * ggchild_count
+ assert len(res_obj.children) == child_count
+ # We must have set the chord sizes based on the number of tail tasks of
+ # the encapsulated groups - in this case `ggchild_count`
+ mock_set_chord_size.assert_has_calls(
+ (call(ANY, ggchild_count), ) * child_count,
+ )
+
+ @pytest.mark.xfail(reason="Invalid canvas setup but poor behaviour")
+ def test_apply_contains_chords_containing_empty_group(self):
+ gchild_sig = group(tuple())
+ child_count = 24
+ child_chord = chord((gchild_sig, ), self.add.si(0, 0))
+ group_sig = group((child_chord, ) * child_count)
+ with patch.object(
+ self.app.backend, "set_chord_size",
+ ) as mock_set_chord_size, patch(
+ "celery.canvas.Signature.apply_async",
+ ) as mock_apply_async:
+ res_obj = group_sig.apply_async()
+ # We only see applies for the header grandchildren because the tasks
+ # are never actually run due to our mocking of `apply_async()`
+ assert mock_apply_async.call_count == child_count
+ assert len(res_obj.children) == child_count
+ # This is actually kind of meaningless because, similar to the empty
+ # chain test, this is an invalid setup. However, we should probably
+ # expect that the chords are dealt with in some other way the probably
+ # being left incomplete forever...
+ mock_set_chord_size.assert_has_calls((call(ANY, 0), ) * child_count)
+
+ def test_apply_contains_chords_containing_chord(self):
+ ggchild_count = 42
+ ggchild_sig = self.add.si(0, 0)
+ gchild_sig = chord((ggchild_sig, ) * ggchild_count, ggchild_sig)
+ child_count = 24
+ child_chord = chord((gchild_sig, ), ggchild_sig)
+ group_sig = group((child_chord, ) * child_count)
+ with patch.object(
+ self.app.backend, "set_chord_size",
+ ) as mock_set_chord_size, patch(
+ "celery.canvas.Signature.apply_async",
+ ) as mock_apply_async:
+ res_obj = group_sig.apply_async()
+ # We see applies for all of the header great-grandchildren because the
+ # tasks are never actually run due to our mocking of `apply_async()`
+ assert mock_apply_async.call_count == child_count * ggchild_count
+ assert len(res_obj.children) == child_count
+ # We must have set the chord sizes based on the number of tail tasks of
+ # the deeply encapsulated chords' header tasks, as well as for each
+ # child chord. This means we have `child_count` interleaved calls to
+ # set chord sizes of 1 and `ggchild_count`.
+ mock_set_chord_size.assert_has_calls(
+ (call(ANY, 1), call(ANY, ggchild_count), ) * child_count,
+ )
+
+ def test_apply_contains_chords_containing_empty_chord(self):
+ gchild_sig = chord(tuple(), self.add.si(0, 0))
+ child_count = 24
+ child_chord = chord((gchild_sig, ), self.add.si(0, 0))
+ group_sig = group((child_chord, ) * child_count)
+ with patch.object(
+ self.app.backend, "set_chord_size",
+ ) as mock_set_chord_size, patch(
+ "celery.canvas.Signature.apply_async",
+ ) as mock_apply_async:
+ res_obj = group_sig.apply_async()
+ # We only see applies for the header grandchildren because the tasks
+ # are never actually run due to our mocking of `apply_async()`
+ assert mock_apply_async.call_count == child_count
+ assert len(res_obj.children) == child_count
+ # We must have set the chord sizes based on the number of tail tasks of
+ # the encapsulated chains - in this case 1 for each child chord
+ mock_set_chord_size.assert_has_calls((call(ANY, 1), ) * child_count)
+
class test_chord(CanvasCase):
From 9edee9330b9decac528494938f29dcbaa6d52ef6 Mon Sep 17 00:00:00 2001
From: jenhaoyang
Date: Wed, 28 Apr 2021 17:49:14 +0800
Subject: [PATCH 213/415] Update periodic-tasks.rst (#6745)
---
docs/userguide/periodic-tasks.rst | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/docs/userguide/periodic-tasks.rst b/docs/userguide/periodic-tasks.rst
index 1e346ed2557..dcc360972ff 100644
--- a/docs/userguide/periodic-tasks.rst
+++ b/docs/userguide/periodic-tasks.rst
@@ -106,6 +106,12 @@ beat schedule list.
@app.task
def test(arg):
print(arg)
+
+ @app.task
+ def add(x, y):
+ z = x + y
+ print(z)
+
Setting these up from within the :data:`~@on_after_configure` handler means
From 934a2271c1636364486eb737a598d224e5184cf8 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 28 Apr 2021 12:53:06 +0300
Subject: [PATCH 214/415] Terminate tasks with late acknoledgement on
connection loss (#6654)
* Terminate tasks with late acknoledgement on connection loss.
* Abort task instead of terminating.
Instead of terminating the task (which revokes it and prevents its execution in the future), abort the task.
* Fix serialization error.
* Remove debugging helpers.
* Avoid revoking the task if it is aborted.
* Rename `abort` to `cancel`.
There already is a concept of abortable tasks so the term is overloaded.
* The revoke flow is no longer called twice.
If the worker already managed to report the task is revoked, there's no need to do it again.
Without this change, the `task-revoked` event and the `task_revoked` signal are sent twice.
* Unify the flow of announcing a task as cancelled.
* Add feature flag.
worker_cancel_long_running_tasks_on_connection_loss is False by default since it is possibly a breaking change.
In 6.0 it will be True by default.
* Add documentation.
* Add unit test for the task cancelling behavior.
* isort.
* Add unit tests for request.cancel().
* isort & autopep8.
* Add test coverage for request.on_failure() changes.
* Add more test coverage.
* Add more test coverage.
---
celery/app/defaults.py | 3 ++
celery/bin/amqp.py | 2 +-
celery/worker/consumer/consumer.py | 40 +++++++++++++---
celery/worker/request.py | 54 +++++++++++++++++----
docs/userguide/configuration.rst | 30 ++++++++++++
t/unit/worker/test_consumer.py | 37 +++++++++++++-
t/unit/worker/test_request.py | 77 +++++++++++++++++++++++++++---
7 files changed, 218 insertions(+), 25 deletions(-)
diff --git a/celery/app/defaults.py b/celery/app/defaults.py
index fcf147f3cdc..8d95712696f 100644
--- a/celery/app/defaults.py
+++ b/celery/app/defaults.py
@@ -290,6 +290,9 @@ def __repr__(self):
__old__=OLD_NS_WORKER,
agent=Option(None, type='string'),
autoscaler=Option('celery.worker.autoscale:Autoscaler'),
+ cancel_long_running_tasks_on_connection_loss=Option(
+ False, type='bool'
+ ),
concurrency=Option(0, type='int'),
consumer=Option('celery.worker.consumer:Consumer', type='string'),
direct=Option(False, type='bool', old={'celery_worker_direct'}),
diff --git a/celery/bin/amqp.py b/celery/bin/amqp.py
index 29c625281ed..d94c91607bd 100644
--- a/celery/bin/amqp.py
+++ b/celery/bin/amqp.py
@@ -25,7 +25,7 @@ def __init__(self, cli_context):
self.connection = self.cli_context.app.connection()
self.channel = None
self.reconnect()
-
+
@property
def app(self):
return self.cli_context.app
diff --git a/celery/worker/consumer/consumer.py b/celery/worker/consumer/consumer.py
index a3fd0afde73..21562528134 100644
--- a/celery/worker/consumer/consumer.py
+++ b/celery/worker/consumer/consumer.py
@@ -7,6 +7,7 @@
import errno
import logging
import os
+import warnings
from collections import defaultdict
from time import sleep
@@ -21,7 +22,8 @@
from celery import bootsteps, signals
from celery.app.trace import build_tracer
-from celery.exceptions import InvalidTaskError, NotRegistered
+from celery.exceptions import (CPendingDeprecationWarning, InvalidTaskError,
+ NotRegistered)
from celery.utils.functional import noop
from celery.utils.log import get_logger
from celery.utils.nodenames import gethostname
@@ -29,8 +31,8 @@
from celery.utils.text import truncate
from celery.utils.time import humanize_seconds, rate
from celery.worker import loops
-from celery.worker.state import (maybe_shutdown, reserved_requests,
- task_reserved)
+from celery.worker.state import (active_requests, maybe_shutdown,
+ reserved_requests, task_reserved)
__all__ = ('Consumer', 'Evloop', 'dump_body')
@@ -106,6 +108,19 @@
delivery_info:{3} headers={4}}}
"""
+TERMINATING_TASK_ON_RESTART_AFTER_A_CONNECTION_LOSS = """\
+Task %s cannot be acknowledged after a connection loss since late acknowledgement is enabled for it.
+Terminating it instead.
+"""
+
+CANCEL_TASKS_BY_DEFAULT = """
+In Celery 5.1 we introduced an optional breaking change which
+on connection loss cancels all currently executed tasks with late acknowledgement enabled.
+These tasks cannot be acknowledged as the connection is gone, and the tasks are automatically redelivered back to the queue.
+You can enable this behavior using the worker_cancel_long_running_tasks_on_connection_loss setting.
+In Celery 5.1 it is set to False by default. The setting will be set to True by default in Celery 6.0.
+"""
+
def dump_body(m, body):
"""Format message body for debugging purposes."""
@@ -257,7 +272,7 @@ def _update_prefetch_count(self, index=0):
def _update_qos_eventually(self, index):
return (self.qos.decrement_eventually if index < 0
else self.qos.increment_eventually)(
- abs(index) * self.prefetch_multiplier)
+ abs(index) * self.prefetch_multiplier)
def _limit_move_to_pool(self, request):
task_reserved(request)
@@ -336,6 +351,15 @@ def on_connection_error_after_connected(self, exc):
except Exception: # pylint: disable=broad-except
pass
+ if self.app.conf.worker_cancel_long_running_tasks_on_connection_loss:
+ for request in tuple(active_requests):
+ if request.task.acks_late and not request.acknowledged:
+ warn(TERMINATING_TASK_ON_RESTART_AFTER_A_CONNECTION_LOSS,
+ request)
+ request.cancel(self.pool)
+ else:
+ warnings.warn(CANCEL_TASKS_BY_DEFAULT, CPendingDeprecationWarning)
+
def register_with_event_loop(self, hub):
self.blueprint.send_all(
self, 'register_with_event_loop', args=(hub,),
@@ -487,7 +511,8 @@ def on_unknown_message(self, body, message):
signals.task_rejected.send(sender=self, message=message, exc=None)
def on_unknown_task(self, body, message, exc):
- error(UNKNOWN_TASK_ERROR, exc, dump_body(message, body), exc_info=True)
+ error(UNKNOWN_TASK_ERROR, exc, dump_body(message, body),
+ exc_info=True)
try:
id_, name = message.headers['id'], message.headers['task']
root_id = message.headers.get('root_id')
@@ -515,7 +540,8 @@ def on_unknown_task(self, body, message, exc):
)
def on_invalid_task(self, body, message, exc):
- error(INVALID_TASK_ERROR, exc, dump_body(message, body), exc_info=True)
+ error(INVALID_TASK_ERROR, exc, dump_body(message, body),
+ exc_info=True)
message.reject_log_error(logger, self.connection_errors)
signals.task_rejected.send(sender=self, message=message, exc=exc)
@@ -539,7 +565,7 @@ def on_task_received(message):
# will defer deserializing the message body to the pool.
payload = None
try:
- type_ = message.headers['task'] # protocol v2
+ type_ = message.headers['task'] # protocol v2
except TypeError:
return on_unknown_message(None, message)
except KeyError:
diff --git a/celery/worker/request.py b/celery/worker/request.py
index 832c6f379ba..487384f256b 100644
--- a/celery/worker/request.py
+++ b/celery/worker/request.py
@@ -55,6 +55,7 @@ def __optimize__():
# Localize
tz_or_local = timezone.tz_or_local
send_revoked = signals.task_revoked.send
+send_retry = signals.task_retry.send
task_accepted = state.task_accepted
task_ready = state.task_ready
@@ -69,6 +70,7 @@ class Request:
worker_pid = None
time_limits = (None, None)
_already_revoked = False
+ _already_cancelled = False
_terminate_on_ack = None
_apply_result = None
_tzlocal = None
@@ -399,6 +401,30 @@ def terminate(self, pool, signal=None):
if obj is not None:
obj.terminate(signal)
+ def cancel(self, pool, signal=None):
+ signal = _signals.signum(signal or TERM_SIGNAME)
+ if self.time_start:
+ pool.terminate_job(self.worker_pid, signal)
+ self._announce_cancelled()
+
+ if self._apply_result is not None:
+ obj = self._apply_result() # is a weakref
+ if obj is not None:
+ obj.terminate(signal)
+
+ def _announce_cancelled(self):
+ task_ready(self)
+ self.send_event('task-cancelled')
+ reason = 'cancelled by Celery'
+ exc = Retry(message=reason)
+ self.task.backend.mark_as_retry(self.id,
+ exc,
+ request=self._context)
+
+ self.task.on_retry(exc, self.id, self.args, self.kwargs, None)
+ self._already_cancelled = True
+ send_retry(self.task, request=self._context, einfo=None)
+
def _announce_revoked(self, reason, terminated, signum, expired):
task_ready(self)
self.send_event('task-revoked',
@@ -492,7 +518,20 @@ def on_failure(self, exc_info, send_failed_event=True, return_ok=False):
task_ready(self)
exc = exc_info.exception
- if isinstance(exc, MemoryError):
+ is_terminated = isinstance(exc, Terminated)
+ if is_terminated:
+ # If the message no longer has a connection and the worker
+ # is terminated, we aborted it.
+ # Otherwise, it is revoked.
+ if self.message.channel.connection and not self._already_revoked:
+ # This is a special case where the process
+ # would not have had time to write the result.
+ self._announce_revoked(
+ 'terminated', True, str(exc), False)
+ elif not self._already_cancelled:
+ self._announce_cancelled()
+ return
+ elif isinstance(exc, MemoryError):
raise MemoryError(f'Process got: {exc}')
elif isinstance(exc, Reject):
return self.reject(requeue=exc.requeue)
@@ -503,10 +542,11 @@ def on_failure(self, exc_info, send_failed_event=True, return_ok=False):
# (acks_late) acknowledge after result stored.
requeue = False
+ is_worker_lost = isinstance(exc, WorkerLostError)
if self.task.acks_late:
reject = (
self.task.reject_on_worker_lost and
- isinstance(exc, WorkerLostError)
+ is_worker_lost
)
ack = self.task.acks_on_failure_or_timeout
if reject:
@@ -520,13 +560,9 @@ def on_failure(self, exc_info, send_failed_event=True, return_ok=False):
# need to be removed from prefetched local queue
self.reject(requeue=False)
- # These are special cases where the process would not have had time
+ # This is a special case where the process would not have had time
# to write the result.
- if isinstance(exc, Terminated):
- self._announce_revoked(
- 'terminated', True, str(exc), False)
- send_failed_event = False # already sent revoked event
- elif not requeue and (isinstance(exc, WorkerLostError) or not return_ok):
+ if not requeue and (is_worker_lost or not return_ok):
# only mark as failure if task has not been requeued
self.task.backend.mark_as_failure(
self.id, exc, request=self._context,
@@ -579,7 +615,7 @@ def __str__(self):
self.humaninfo(),
f' ETA:[{self._eta}]' if self._eta else '',
f' expires:[{self._expires}]' if self._expires else '',
- ])
+ ]).strip()
def __repr__(self):
"""``repr(self)``."""
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index e653b0d82d0..5110e476bf7 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -2735,6 +2735,36 @@ Default: 4.0.
The timeout in seconds (int/float) when waiting for a new worker process to start up.
+.. setting:: worker_cancel_long_running_tasks_on_connection_loss
+
+``worker_cancel_long_running_tasks_on_connection_loss``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 5.1
+
+Default: Disabled by default.
+
+Kill all long-running tasks with late acknowledgment enabled on connection loss.
+
+Tasks which have not been acknowledged before the connection loss cannot do so
+anymore since their channel is gone and the task is redelivered back to the queue.
+This is why tasks with late acknowledged enabled must be idempotent as they may be executed more than once.
+In this case, the task is being executed twice per connection loss (and sometimes in parallel in other workers).
+
+When turning this option on, those tasks which have not been completed are
+cancelled and their execution is terminated.
+Tasks which have completed in any way before the connection loss
+are recorded as such in the result backend as long as :setting:`task_ignore_result` is not enabled.
+
+.. warning::
+
+ This feature was introduced as a future breaking change.
+ If it is turned off, Celery will emit a warning message.
+
+ In Celery 6.0, the :setting:`worker_cancel_long_running_tasks_on_connection_loss`
+ will be set to ``True`` by default as the current behavior leads to more
+ problems than it solves.
+
.. _conf-events:
Events
diff --git a/t/unit/worker/test_consumer.py b/t/unit/worker/test_consumer.py
index f7530ef6b37..a11098f37fa 100644
--- a/t/unit/worker/test_consumer.py
+++ b/t/unit/worker/test_consumer.py
@@ -9,11 +9,13 @@
from celery.utils.collections import LimitedSet
from celery.worker.consumer.agent import Agent
-from celery.worker.consumer.consumer import CLOSE, TERMINATE, Consumer
+from celery.worker.consumer.consumer import (CANCEL_TASKS_BY_DEFAULT, CLOSE,
+ TERMINATE, Consumer)
from celery.worker.consumer.gossip import Gossip
from celery.worker.consumer.heart import Heart
from celery.worker.consumer.mingle import Mingle
from celery.worker.consumer.tasks import Tasks
+from celery.worker.state import active_requests
class test_Consumer:
@@ -272,6 +274,39 @@ def test_connect_error_handler_progress(self, error):
errback(Mock(), 6)
assert error.call_args[0][3] == 'Trying again in 6.00 seconds... (3/3)'
+ def test_cancel_long_running_tasks_on_connection_loss(self):
+ c = self.get_consumer()
+ c.app.conf.worker_cancel_long_running_tasks_on_connection_loss = True
+
+ mock_request_acks_late_not_acknowledged = Mock()
+ mock_request_acks_late_not_acknowledged.task.acks_late = True
+ mock_request_acks_late_not_acknowledged.acknowledged = False
+ mock_request_acks_late_acknowledged = Mock()
+ mock_request_acks_late_acknowledged.task.acks_late = True
+ mock_request_acks_late_acknowledged.acknowledged = True
+ mock_request_acks_early = Mock()
+ mock_request_acks_early.task.acks_late = False
+ mock_request_acks_early.acknowledged = False
+
+ active_requests.add(mock_request_acks_late_not_acknowledged)
+ active_requests.add(mock_request_acks_late_acknowledged)
+ active_requests.add(mock_request_acks_early)
+
+ c.on_connection_error_after_connected(Mock())
+
+ mock_request_acks_late_not_acknowledged.cancel.assert_called_once_with(c.pool)
+ mock_request_acks_late_acknowledged.cancel.assert_not_called()
+ mock_request_acks_early.cancel.assert_not_called()
+
+ active_requests.clear()
+
+ def test_cancel_long_running_tasks_on_connection_loss__warning(self):
+ c = self.get_consumer()
+ c.app.conf.worker_cancel_long_running_tasks_on_connection_loss = False
+
+ with pytest.deprecated_call(match=CANCEL_TASKS_BY_DEFAULT):
+ c.on_connection_error_after_connected(Mock())
+
class test_Heart:
diff --git a/t/unit/worker/test_request.py b/t/unit/worker/test_request.py
index d8f7de6ad1d..c84c00f628f 100644
--- a/t/unit/worker/test_request.py
+++ b/t/unit/worker/test_request.py
@@ -19,7 +19,7 @@
from celery.backends.base import BaseDictBackend
from celery.exceptions import (Ignore, InvalidTaskError, Reject, Retry,
TaskRevokedError, Terminated, WorkerLostError)
-from celery.signals import task_revoked
+from celery.signals import task_retry, task_revoked
from celery.worker import request as module
from celery.worker import strategy
from celery.worker.request import Request, create_request_cls
@@ -35,16 +35,19 @@ def setup(self):
@self.app.task(shared=False)
def add(x, y, **kw_):
return x + y
+
self.add = add
@self.app.task(shared=False)
def mytask(i, **kwargs):
return i ** i
+
self.mytask = mytask
@self.app.task(shared=False)
def mytask_raising(i):
raise KeyError(i)
+
self.mytask_raising = mytask_raising
def xRequest(self, name=None, id=None, args=None, kwargs=None,
@@ -63,7 +66,6 @@ def xRequest(self, name=None, id=None, args=None, kwargs=None,
class test_mro_lookup:
def test_order(self):
-
class A:
pass
@@ -137,6 +139,7 @@ def test_marked_as_started(self):
def store_result(tid, meta, state, **kwargs):
if state == states.STARTED:
_started.append(tid)
+
self.mytask.backend.store_result = Mock(name='store_result')
self.mytask.backend.store_result.side_effect = store_result
self.mytask.track_started = True
@@ -158,7 +161,6 @@ def test_execute_jail_failure(self):
assert ret.exception.args == (4,)
def test_execute_task_ignore_result(self):
-
@self.app.task(shared=False, ignore_result=True)
def ignores_result(i):
return i ** i
@@ -227,14 +229,16 @@ def test_info_function(self):
import string
kwargs = {}
for i in range(0, 2):
- kwargs[str(i)] = ''.join(random.choice(string.ascii_lowercase) for i in range(1000))
+ kwargs[str(i)] = ''.join(
+ random.choice(string.ascii_lowercase) for i in range(1000))
assert self.get_request(
self.add.s(**kwargs)).info(safe=True).get('kwargs') == kwargs
assert self.get_request(
self.add.s(**kwargs)).info(safe=False).get('kwargs') == kwargs
args = []
for i in range(0, 2):
- args.append(''.join(random.choice(string.ascii_lowercase) for i in range(1000)))
+ args.append(''.join(
+ random.choice(string.ascii_lowercase) for i in range(1000)))
assert list(self.get_request(
self.add.s(*args)).info(safe=True).get('args')) == args
assert list(self.get_request(
@@ -449,6 +453,23 @@ def test_terminate__task_started(self):
job.terminate(pool, signal='TERM')
pool.terminate_job.assert_called_with(job.worker_pid, signum)
+ def test_cancel__pool_ref(self):
+ pool = Mock()
+ signum = signal.SIGTERM
+ job = self.get_request(self.mytask.s(1, f='x'))
+ job._apply_result = Mock(name='_apply_result')
+ with self.assert_signal_called(
+ task_retry, sender=job.task, request=job._context,
+ einfo=None):
+ job.time_start = monotonic()
+ job.worker_pid = 314
+ job.cancel(pool, signal='TERM')
+ job._apply_result().terminate.assert_called_with(signum)
+
+ job._apply_result = Mock(name='_apply_result2')
+ job._apply_result.return_value = None
+ job.cancel(pool, signal='TERM')
+
def test_terminate__task_reserved(self):
pool = Mock()
job = self.get_request(self.mytask.s(1, f='x'))
@@ -458,6 +479,27 @@ def test_terminate__task_reserved(self):
assert job._terminate_on_ack == (pool, 15)
job.terminate(pool, signal='TERM')
+ def test_cancel__task_started(self):
+ pool = Mock()
+ signum = signal.SIGTERM
+ job = self.get_request(self.mytask.s(1, f='x'))
+ job._apply_result = Mock(name='_apply_result')
+ with self.assert_signal_called(
+ task_retry, sender=job.task, request=job._context,
+ einfo=None):
+ job.time_start = monotonic()
+ job.worker_pid = 314
+ job.cancel(pool, signal='TERM')
+ job._apply_result().terminate.assert_called_with(signum)
+
+ def test_cancel__task_reserved(self):
+ pool = Mock()
+ job = self.get_request(self.mytask.s(1, f='x'))
+ job.time_start = None
+ job.cancel(pool, signal='TERM')
+ pool.terminate_job.assert_not_called()
+ assert job._terminate_on_ack is None
+
def test_revoked_expires_expired(self):
job = self.get_request(self.mytask.s(1, f='x').set(
expires=datetime.utcnow() - timedelta(days=1)
@@ -667,7 +709,8 @@ def test_on_failure_acks_on_failure_or_timeout_disabled_for_task(self):
job.on_failure(exc_info)
assert job.acknowledged is True
- job._on_reject.assert_called_with(req_logger, job.connection_errors, False)
+ job._on_reject.assert_called_with(req_logger, job.connection_errors,
+ False)
def test_on_failure_acks_on_failure_or_timeout_enabled_for_task(self):
job = self.xRequest()
@@ -709,6 +752,25 @@ def test_on_failure_acks_on_failure_or_timeout_enabled(self):
job.on_failure(exc_info)
assert job.acknowledged is True
+ def test_on_failure_task_cancelled(self):
+ job = self.xRequest()
+ job.eventer = Mock()
+ job.time_start = 1
+ job.message.channel.connection = None
+
+ try:
+ raise Terminated()
+ except Terminated:
+ exc_info = ExceptionInfo()
+
+ job.on_failure(exc_info)
+
+ assert job._already_cancelled
+
+ job.on_failure(exc_info)
+ job.eventer.send.assert_called_once_with('task-cancelled',
+ uuid=job.id)
+
def test_from_message_invalid_kwargs(self):
m = self.TaskMessage(self.mytask.name, args=(), kwargs='foo')
req = Request(m, app=self.app)
@@ -1087,7 +1149,8 @@ def setup(self):
def create_request_cls(self, **kwargs):
return create_request_cls(
- Request, self.task, self.pool, 'foo', self.eventer, app=self.app, **kwargs
+ Request, self.task, self.pool, 'foo', self.eventer, app=self.app,
+ **kwargs
)
def zRequest(self, Request=None, revoked_tasks=None, ref=None, **kwargs):
From 8d6778810c5153c9e4667eed618de2d0bf72663e Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 28 Apr 2021 18:37:08 +0300
Subject: [PATCH 215/415] Deduplicate successful tasks (#6722)
* Deduplicate successful tasks.
This feature allows the user to deduplicate successful tasks which acks late.
The trace function fetches the metadata from the backend each time it receives a task and compares its state.
If the state is SUCCESS we log and bail instead of executing the task.
The task is acknowledged and everything proceeds normally.
* Fix test to cover a backend error.
* Added a local cache of successful task.
Instead of hitting the backend every time, we first check if the task was successfully executed in this worker.
The local cache is limited to 1000 tasks so our memory usage won't grow dramatically over time.
* Only deduplicate when task is redelivered.
* Don't deduplicate when backend is not persistent.
* Added documentation.
* Push the task into the stack only after checking that it is not a duplicate.
* Adjust unit tests.
---
celery/app/defaults.py | 3 +
celery/app/trace.py | 34 +++++++++-
celery/worker/request.py | 2 +-
celery/worker/state.py | 16 +++++
docs/userguide/configuration.rst | 27 ++++++++
t/unit/tasks/test_trace.py | 106 +++++++++++++++++++++++++++++--
6 files changed, 179 insertions(+), 9 deletions(-)
diff --git a/celery/app/defaults.py b/celery/app/defaults.py
index 8d95712696f..abb46cca8dd 100644
--- a/celery/app/defaults.py
+++ b/celery/app/defaults.py
@@ -299,6 +299,9 @@ def __repr__(self):
disable_rate_limits=Option(
False, type='bool', old={'celery_disable_rate_limits'},
),
+ deduplicate_successful_tasks=Option(
+ False, type='bool'
+ ),
enable_remote_control=Option(
True, type='bool', old={'celery_enable_remote_control'},
),
diff --git a/celery/app/trace.py b/celery/app/trace.py
index b6ff79fcef5..fb4fdd6d7e5 100644
--- a/celery/app/trace.py
+++ b/celery/app/trace.py
@@ -20,7 +20,9 @@
from celery._state import _task_stack
from celery.app.task import Context
from celery.app.task import Task as BaseTask
-from celery.exceptions import Ignore, InvalidTaskError, Reject, Retry
+from celery.exceptions import (BackendGetMetaError, Ignore, InvalidTaskError,
+ Reject, Retry)
+from celery.result import AsyncResult
from celery.utils.log import get_logger
from celery.utils.nodenames import gethostname
from celery.utils.objects import mro_lookup
@@ -46,6 +48,8 @@
'setup_worker_optimizations', 'reset_worker_optimizations',
)
+from celery.worker.state import successful_requests
+
logger = get_logger(__name__)
#: Format string used to log task success.
@@ -327,6 +331,10 @@ def build_tracer(name, task, loader=None, hostname=None, store_errors=True,
else:
publish_result = not eager and not ignore_result
+ deduplicate_successful_tasks = ((app.conf.task_acks_late or task.acks_late)
+ and app.conf.worker_deduplicate_successful_tasks
+ and app.backend.persistent)
+
hostname = hostname or gethostname()
inherit_parent_priority = app.conf.task_inherit_parent_priority
@@ -391,9 +399,31 @@ def trace_task(uuid, args, kwargs, request=None):
except AttributeError:
raise InvalidTaskError(
'Task keyword arguments is not a mapping')
- push_task(task)
+
task_request = Context(request or {}, args=args,
called_directly=False, kwargs=kwargs)
+
+ redelivered = (task_request.delivery_info
+ and task_request.delivery_info.get('redelivered', False))
+ if deduplicate_successful_tasks and redelivered:
+ if task_request.id in successful_requests:
+ return trace_ok_t(R, I, T, Rstr)
+ r = AsyncResult(task_request.id, app=app)
+
+ try:
+ state = r.state
+ except BackendGetMetaError:
+ pass
+ else:
+ if state == SUCCESS:
+ info(LOG_IGNORED, {
+ 'id': task_request.id,
+ 'name': get_task_name(task_request, name),
+ 'description': 'Task already completed successfully.'
+ })
+ return trace_ok_t(R, I, T, Rstr)
+
+ push_task(task)
root_id = task_request.root_id or uuid
task_priority = task_request.delivery_info.get('priority') if \
inherit_parent_priority else None
diff --git a/celery/worker/request.py b/celery/worker/request.py
index 487384f256b..2255de132b1 100644
--- a/celery/worker/request.py
+++ b/celery/worker/request.py
@@ -497,7 +497,7 @@ def on_success(self, failed__retval__runtime, **kwargs):
if isinstance(retval.exception, (SystemExit, KeyboardInterrupt)):
raise retval.exception
return self.on_failure(retval, return_ok=True)
- task_ready(self)
+ task_ready(self, successful=True)
if self.task.acks_late:
self.acknowledge()
diff --git a/celery/worker/state.py b/celery/worker/state.py
index aa8782546c4..5b2ed68c5fe 100644
--- a/celery/worker/state.py
+++ b/celery/worker/state.py
@@ -34,10 +34,17 @@
#: maximum number of revokes to keep in memory.
REVOKES_MAX = 50000
+#: maximum number of successful tasks to keep in memory.
+SUCCESSFUL_MAX = 1000
+
#: how many seconds a revoke will be active before
#: being expired when the max limit has been exceeded.
REVOKE_EXPIRES = 10800
+#: how many seconds a successful task will be cached in memory
+#: before being expired when the max limit has been exceeded.
+SUCCESSFUL_EXPIRES = 10800
+
#: Mapping of reserved task_id->Request.
requests = {}
@@ -47,6 +54,10 @@
#: set of currently active :class:`~celery.worker.request.Request`'s.
active_requests = weakref.WeakSet()
+#: A limited set of successful :class:`~celery.worker.request.Request`'s.
+successful_requests = LimitedSet(maxlen=SUCCESSFUL_MAX,
+ expires=SUCCESSFUL_EXPIRES)
+
#: count of tasks accepted by the worker, sorted by type.
total_count = Counter()
@@ -64,6 +75,7 @@ def reset_state():
requests.clear()
reserved_requests.clear()
active_requests.clear()
+ successful_requests.clear()
total_count.clear()
all_total_count[:] = [0]
revoked.clear()
@@ -98,10 +110,14 @@ def task_accepted(request,
def task_ready(request,
+ successful=False,
remove_request=requests.pop,
discard_active_request=active_requests.discard,
discard_reserved_request=reserved_requests.discard):
"""Update global state when a task is ready."""
+ if successful:
+ successful_requests.add(request.id)
+
remove_request(request.id, None)
discard_active_request(request)
discard_reserved_request(request)
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index 5110e476bf7..d2ae1e2a166 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -2601,6 +2601,33 @@ to have different import categories.
The modules in this setting are imported after the modules in
:setting:`imports`.
+.. setting:: worker_deduplicate_successful_tasks
+
+``worker_deduplicate_successful_tasks``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 5.1
+
+Default: False
+
+Before each task execution, instruct the worker to check if this task is
+a duplicate message.
+
+Deduplication occurs only with tasks that have the same identifier,
+enabled late acknowledgment, were redelivered by the message broker
+and their state is ``SUCCESS`` in the result backend.
+
+To avoid overflowing the result backend with queries, a local cache of
+successfully executed tasks is checked before querying the result backend
+in case the task was already successfully executed by the same worker that
+received the task.
+
+This cache can be made persistent by setting the :setting:`worker_state_db`
+setting.
+
+If the result backend is not persistent (the RPC backend, for example),
+this setting is ignored.
+
.. _conf-concurrency:
.. setting:: worker_concurrency
diff --git a/t/unit/tasks/test_trace.py b/t/unit/tasks/test_trace.py
index c7e11552976..d5cb86ec455 100644
--- a/t/unit/tasks/test_trace.py
+++ b/t/unit/tasks/test_trace.py
@@ -1,4 +1,5 @@
-from unittest.mock import ANY, Mock, patch
+from unittest.mock import ANY, Mock, PropertyMock, patch
+from uuid import uuid4
import pytest
from billiard.einfo import ExceptionInfo
@@ -6,8 +7,8 @@
from celery import group, signals, states, uuid
from celery.app.task import Context
-from celery.app.trace import (TraceInfo, build_tracer, fast_trace_task,
- get_log_policy, get_task_name,
+from celery.app.trace import (TraceInfo, build_tracer,
+ fast_trace_task, get_log_policy, get_task_name,
log_policy_expected, log_policy_ignore,
log_policy_internal, log_policy_reject,
log_policy_unexpected,
@@ -15,14 +16,18 @@
setup_worker_optimizations, trace_task,
trace_task_ret, traceback_clear)
from celery.backends.base import BaseDictBackend
-from celery.exceptions import Ignore, Reject, Retry
+from celery.backends.cache import CacheBackend
+from celery.exceptions import BackendGetMetaError, Ignore, Reject, Retry
+from celery.states import PENDING
+from celery.worker.state import successful_requests
def trace(
- app, task, args=(), kwargs={}, propagate=False, eager=True, request=None, **opts
+ app, task, args=(), kwargs={}, propagate=False,
+ eager=True, request=None, task_id='id-1', **opts
):
t = build_tracer(task.name, task, eager=eager, propagate=propagate, app=app, **opts)
- ret = t('id-1', args, kwargs, request)
+ ret = t(task_id, args, kwargs, request)
return ret.retval, ret.info
@@ -466,6 +471,95 @@ def xtask():
assert info is not None
assert isinstance(ret, ExceptionInfo)
+ def test_deduplicate_successful_tasks__deduplication(self):
+ @self.app.task(shared=False)
+ def add(x, y):
+ return x + y
+
+ backend = CacheBackend(app=self.app, backend='memory')
+ add.backend = backend
+ add.store_eager_result = True
+ add.ignore_result = False
+ add.acks_late = True
+
+ self.app.conf.worker_deduplicate_successful_tasks = True
+ task_id = str(uuid4())
+ request = {'id': task_id, 'delivery_info': {'redelivered': True}}
+
+ assert trace(self.app, add, (1, 1), task_id=task_id, request=request) == (2, None)
+ assert trace(self.app, add, (1, 1), task_id=task_id, request=request) == (None, None)
+
+ self.app.conf.worker_deduplicate_successful_tasks = False
+
+ def test_deduplicate_successful_tasks__no_deduplication(self):
+ @self.app.task(shared=False)
+ def add(x, y):
+ return x + y
+
+ backend = CacheBackend(app=self.app, backend='memory')
+ add.backend = backend
+ add.store_eager_result = True
+ add.ignore_result = False
+ add.acks_late = True
+
+ self.app.conf.worker_deduplicate_successful_tasks = True
+ task_id = str(uuid4())
+ request = {'id': task_id, 'delivery_info': {'redelivered': True}}
+
+ with patch('celery.app.trace.AsyncResult') as async_result_mock:
+ async_result_mock().state.return_value = PENDING
+ assert trace(self.app, add, (1, 1), task_id=task_id, request=request) == (2, None)
+ assert trace(self.app, add, (1, 1), task_id=task_id, request=request) == (2, None)
+
+ self.app.conf.worker_deduplicate_successful_tasks = False
+
+ def test_deduplicate_successful_tasks__result_not_found(self):
+ @self.app.task(shared=False)
+ def add(x, y):
+ return x + y
+
+ backend = CacheBackend(app=self.app, backend='memory')
+ add.backend = backend
+ add.store_eager_result = True
+ add.ignore_result = False
+ add.acks_late = True
+
+ self.app.conf.worker_deduplicate_successful_tasks = True
+ task_id = str(uuid4())
+ request = {'id': task_id, 'delivery_info': {'redelivered': True}}
+
+ with patch('celery.app.trace.AsyncResult') as async_result_mock:
+ assert trace(self.app, add, (1, 1), task_id=task_id, request=request) == (2, None)
+ state_property = PropertyMock(side_effect=BackendGetMetaError)
+ type(async_result_mock()).state = state_property
+ assert trace(self.app, add, (1, 1), task_id=task_id, request=request) == (2, None)
+
+ self.app.conf.worker_deduplicate_successful_tasks = False
+
+ def test_deduplicate_successful_tasks__cached_request(self):
+ @self.app.task(shared=False)
+ def add(x, y):
+ return x + y
+
+ backend = CacheBackend(app=self.app, backend='memory')
+ add.backend = backend
+ add.store_eager_result = True
+ add.ignore_result = False
+ add.acks_late = True
+
+ self.app.conf.worker_deduplicate_successful_tasks = True
+
+ task_id = str(uuid4())
+ request = {'id': task_id, 'delivery_info': {'redelivered': True}}
+
+ successful_requests.add(task_id)
+
+ assert trace(self.app, add, (1, 1), task_id=task_id,
+ request=request) == (None, None)
+
+ successful_requests.clear()
+ self.app.conf.worker_deduplicate_successful_tasks = False
+
class test_TraceInfo(TraceCase):
class TI(TraceInfo):
From b0326ab0e249288e8e551e78fcb88ab2c2b84bcb Mon Sep 17 00:00:00 2001
From: Sergey Tikhonov
Date: Thu, 29 Apr 2021 17:15:04 +0300
Subject: [PATCH 216/415] #6748 Fix Retry.__reduce__ method (#6749)
* #6748 Fix Retry.__reduce__ method
* #6748 ensure that Retry.exc is pickleable in __reduce__
* #6748 fix maximum recursion for pypy, remove pickleable exception.
get_pickleable_exception introduces circular import
* #6748 remove arguments missing in pickled Retry instance
* #6748 optimize imports
---
celery/exceptions.py | 2 +-
t/unit/app/test_exceptions.py | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/celery/exceptions.py b/celery/exceptions.py
index a30f460c69a..cc09d3f894c 100644
--- a/celery/exceptions.py
+++ b/celery/exceptions.py
@@ -180,7 +180,7 @@ def __str__(self):
return f'Retry {self.humanize()}'
def __reduce__(self):
- return self.__class__, (self.message, self.excs, self.when)
+ return self.__class__, (self.message, self.exc, self.when)
RetryTaskError = Retry # noqa: E305 XXX compat
diff --git a/t/unit/app/test_exceptions.py b/t/unit/app/test_exceptions.py
index 3b42a0bed55..b881be4c028 100644
--- a/t/unit/app/test_exceptions.py
+++ b/t/unit/app/test_exceptions.py
@@ -12,7 +12,10 @@ def test_when_datetime(self):
def test_pickleable(self):
x = Retry('foo', KeyError(), when=datetime.utcnow())
- assert pickle.loads(pickle.dumps(x))
+ y = pickle.loads(pickle.dumps(x))
+ assert x.message == y.message
+ assert repr(x.exc) == repr(y.exc)
+ assert x.when == y.when
class test_Reject:
From ae20f2fcc8553af25f15699fe41a07a3e5db19a8 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 2 May 2021 11:33:54 +0300
Subject: [PATCH 217/415] Add support in the redis result backend for
authenticating with a username (#6750)
* Add support in the redis result backend for authenticating with a username.
Previously, the username was ignored from the URI.
Starting from Redis>=6.0, that shouldn't be the case since ACL support has landed.
Fixes #6422.
* Mention which version added support for this setting.
---
celery/app/defaults.py | 1 +
celery/backends/redis.py | 17 ++++++++++++++---
docs/userguide/configuration.rst | 19 +++++++++++++++++--
t/unit/backends/test_redis.py | 27 +++++++++++++++++++++++++++
4 files changed, 59 insertions(+), 5 deletions(-)
diff --git a/celery/app/defaults.py b/celery/app/defaults.py
index abb46cca8dd..1883f2565bb 100644
--- a/celery/app/defaults.py
+++ b/celery/app/defaults.py
@@ -176,6 +176,7 @@ def __repr__(self):
db=Option(type='int'),
host=Option(type='string'),
max_connections=Option(type='int'),
+ username=Option(type='string'),
password=Option(type='string'),
port=Option(type='int'),
socket_timeout=Option(120.0, type='float'),
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index 74a2e18b582..a52cf33d519 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -233,6 +233,17 @@ def __init__(self, host=None, port=None, db=None, password=None,
socket_connect_timeout and float(socket_connect_timeout),
}
+ username = _get('redis_username')
+ if username:
+ # We're extra careful to avoid including this configuration value
+ # if it wasn't specified since older versions of py-redis
+ # don't support specifying a username.
+ # Only Redis>6.0 supports username/password authentication.
+
+ # TODO: Include this in connparams' definition once we drop
+ # support for py-redis<3.4.0.
+ self.connparams['username'] = username
+
if health_check_interval:
self.connparams["health_check_interval"] = health_check_interval
@@ -285,11 +296,11 @@ def __init__(self, host=None, port=None, db=None, password=None,
)
def _params_from_url(self, url, defaults):
- scheme, host, port, _, password, path, query = _parse_url(url)
+ scheme, host, port, username, password, path, query = _parse_url(url)
connparams = dict(
defaults, **dictfilter({
- 'host': host, 'port': port, 'password': password,
- 'db': query.pop('virtual_host', None)})
+ 'host': host, 'port': port, 'username': username,
+ 'password': password, 'db': query.pop('virtual_host', None)})
)
if scheme == 'socket':
diff --git a/docs/userguide/configuration.rst b/docs/userguide/configuration.rst
index d2ae1e2a166..739dc5680c4 100644
--- a/docs/userguide/configuration.rst
+++ b/docs/userguide/configuration.rst
@@ -107,6 +107,7 @@ have been moved into a new ``task_`` prefix.
``CELERY_REDIS_DB`` :setting:`redis_db`
``CELERY_REDIS_HOST`` :setting:`redis_host`
``CELERY_REDIS_MAX_CONNECTIONS`` :setting:`redis_max_connections`
+``CELERY_REDIS_USERNAME`` :setting:`redis_username`
``CELERY_REDIS_PASSWORD`` :setting:`redis_password`
``CELERY_REDIS_PORT`` :setting:`redis_port`
``CELERY_REDIS_BACKEND_USE_SSL`` :setting:`redis_backend_use_ssl`
@@ -1127,7 +1128,7 @@ Configuring the backend URL
This backend requires the :setting:`result_backend`
setting to be set to a Redis or `Redis over TLS`_ URL::
- result_backend = 'redis://:password@host:port/db'
+ result_backend = 'redis://username:password@host:port/db'
.. _`Redis over TLS`:
https://www.iana.org/assignments/uri-schemes/prov/rediss
@@ -1142,7 +1143,7 @@ is the same as::
Use the ``rediss://`` protocol to connect to redis over TLS::
- result_backend = 'rediss://:password@host:port/db?ssl_cert_reqs=required'
+ result_backend = 'rediss://username:password@host:port/db?ssl_cert_reqs=required'
Note that the ``ssl_cert_reqs`` string should be one of ``required``,
``optional``, or ``none`` (though, for backwards compatibility, the string
@@ -1154,6 +1155,20 @@ If a Unix socket connection should be used, the URL needs to be in the format:::
The fields of the URL are defined as follows:
+#. ``username``
+
+ .. versionadded:: 5.1.0
+
+ Username used to connect to the database.
+
+ Note that this is only supported in Redis>=6.0 and with py-redis>=3.4.0
+ installed.
+
+ If you use an older database version or an older client version
+ you can omit the username::
+
+ result_backend = 'redis://:password@host:port/db'
+
#. ``password``
Password used to connect to the database.
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index a33fce329ca..c96bcca357a 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -340,6 +340,20 @@ def test_no_redis(self):
with pytest.raises(ImproperlyConfigured):
self.Backend(app=self.app)
+ def test_username_password_from_redis_conf(self):
+ self.app.conf.redis_password = 'password'
+ x = self.Backend(app=self.app)
+
+ assert x.connparams
+ assert 'username' not in x.connparams
+ assert x.connparams['password'] == 'password'
+ self.app.conf.redis_username = 'username'
+ x = self.Backend(app=self.app)
+
+ assert x.connparams
+ assert x.connparams['username'] == 'username'
+ assert x.connparams['password'] == 'password'
+
def test_url(self):
self.app.conf.redis_socket_timeout = 30.0
self.app.conf.redis_socket_connect_timeout = 100.0
@@ -353,6 +367,19 @@ def test_url(self):
assert x.connparams['password'] == 'bosco'
assert x.connparams['socket_timeout'] == 30.0
assert x.connparams['socket_connect_timeout'] == 100.0
+ assert 'username' not in x.connparams
+
+ x = self.Backend(
+ 'redis://username:bosco@vandelay.com:123//1', app=self.app,
+ )
+ assert x.connparams
+ assert x.connparams['host'] == 'vandelay.com'
+ assert x.connparams['db'] == 1
+ assert x.connparams['port'] == 123
+ assert x.connparams['username'] == 'username'
+ assert x.connparams['password'] == 'bosco'
+ assert x.connparams['socket_timeout'] == 30.0
+ assert x.connparams['socket_connect_timeout'] == 100.0
def test_timeouts_in_url_coerced(self):
pytest.importorskip('redis')
From e1e139e773c7826a6e0d56395fb3af8bfb7b98bb Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 2 May 2021 12:57:16 +0300
Subject: [PATCH 218/415] worker_pool setting is now respected correctly
(#6711)
* Set all remaining kwargs as preconfigured settings.
This avoids confusing our users when they set other settings through the Celery constructor.
* Prefer the worker_pool setting if available.
If we get the default value through the CLI, we should first check if the worker_pool setting was set.
Fixes #6701.
* Added unit test for configuration using kwargs.
---
celery/app/base.py | 7 ++++---
celery/bin/worker.py | 28 ++++++++++++++++++++++------
t/unit/app/test_app.py | 4 ++++
3 files changed, 30 insertions(+), 9 deletions(-)
diff --git a/celery/app/base.py b/celery/app/base.py
index 27f1d90f779..f0b45694e4f 100644
--- a/celery/app/base.py
+++ b/celery/app/base.py
@@ -274,9 +274,10 @@ def __init__(self, main=None, loader=None, backend=None,
self.__autoset('broker_url', broker)
self.__autoset('result_backend', backend)
self.__autoset('include', include)
- self.__autoset('broker_use_ssl', kwargs.get('broker_use_ssl'))
- self.__autoset('redis_backend_use_ssl',
- kwargs.get('redis_backend_use_ssl'))
+
+ for key, value in kwargs.items():
+ self.__autoset(key, value)
+
self._conf = Settings(
PendingConfiguration(
self._preconf, self._finalize_pending_conf),
diff --git a/celery/bin/worker.py b/celery/bin/worker.py
index 5b5e7fd8ed3..7242706f748 100644
--- a/celery/bin/worker.py
+++ b/celery/bin/worker.py
@@ -45,8 +45,20 @@ def __init__(self):
def convert(self, value, param, ctx):
# Pools like eventlet/gevent needs to patch libs as early
# as possible.
- return concurrency.get_implementation(
- value) or ctx.obj.app.conf.worker_pool
+ value = super().convert(value, param, ctx)
+ worker_pool = ctx.obj.app.conf.worker_pool
+ if value == 'prefork' and worker_pool:
+ # If we got the default pool through the CLI
+ # we need to check if the worker pool was configured.
+ # If the worker pool was configured, we shouldn't use the default.
+ value = concurrency.get_implementation(worker_pool)
+ else:
+ value = concurrency.get_implementation(value)
+
+ if not value:
+ value = concurrency.get_implementation(worker_pool)
+
+ return value
class Hostname(StringParamType):
@@ -140,7 +152,8 @@ def detach(path, argv, logfile=None, pidfile=None, uid=None,
'--statedb',
cls=CeleryOption,
type=click.Path(),
- callback=lambda ctx, _, value: value or ctx.obj.app.conf.worker_state_db,
+ callback=lambda ctx, _,
+ value: value or ctx.obj.app.conf.worker_state_db,
help_group="Worker Options",
help="Path to the state database. The extension '.db' may be "
"appended to the filename.")
@@ -161,7 +174,8 @@ def detach(path, argv, logfile=None, pidfile=None, uid=None,
@click.option('--prefetch-multiplier',
type=int,
metavar="",
- callback=lambda ctx, _, value: value or ctx.obj.app.conf.worker_prefetch_multiplier,
+ callback=lambda ctx, _,
+ value: value or ctx.obj.app.conf.worker_prefetch_multiplier,
cls=CeleryOption,
help_group="Worker Options",
help="Set custom prefetch multiplier value"
@@ -170,7 +184,8 @@ def detach(path, argv, logfile=None, pidfile=None, uid=None,
'--concurrency',
type=int,
metavar="",
- callback=lambda ctx, _, value: value or ctx.obj.app.conf.worker_concurrency,
+ callback=lambda ctx, _,
+ value: value or ctx.obj.app.conf.worker_concurrency,
cls=CeleryOption,
help_group="Pool Options",
help="Number of child processes processing the queue. "
@@ -268,7 +283,8 @@ def detach(path, argv, logfile=None, pidfile=None, uid=None,
@click.option('-s',
'--schedule-filename',
'--schedule',
- callback=lambda ctx, _, value: value or ctx.obj.app.conf.beat_schedule_filename,
+ callback=lambda ctx, _,
+ value: value or ctx.obj.app.conf.beat_schedule_filename,
cls=CeleryOption,
help_group="Embedded Beat Options")
@click.option('--scheduler',
diff --git a/t/unit/app/test_app.py b/t/unit/app/test_app.py
index 5178cbdf59b..0cfadb1800e 100644
--- a/t/unit/app/test_app.py
+++ b/t/unit/app/test_app.py
@@ -274,6 +274,10 @@ def test_with_broker(self, patching):
with self.Celery(broker='foo://baribaz') as app:
assert app.conf.broker_url == 'foo://baribaz'
+ def test_pending_confugration__kwargs(self):
+ with self.Celery(foo='bar') as app:
+ assert app.conf.foo == 'bar'
+
def test_pending_configuration__setattr(self):
with self.Celery(broker='foo://bar') as app:
app.conf.task_default_delivery_mode = 44
From 9dee18bfbacffbc6f04d61745d20e917a304c1b5 Mon Sep 17 00:00:00 2001
From: Jonas Kittner <54631600+theendlessriver13@users.noreply.github.com>
Date: Mon, 3 May 2021 10:26:40 +0200
Subject: [PATCH 219/415] update docs/userguide - `@task` -> `@app.task`
(#6752)
---
docs/userguide/tasks.rst | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/userguide/tasks.rst b/docs/userguide/tasks.rst
index e41da045ea7..1870d8e1a7c 100644
--- a/docs/userguide/tasks.rst
+++ b/docs/userguide/tasks.rst
@@ -153,7 +153,7 @@ be the task instance (``self``), just like Python bound methods:
logger = get_task_logger(__name__)
- @task(bind=True)
+ @app.task(bind=True)
def add(self, x, y):
logger.info(self.request.id)
@@ -175,7 +175,7 @@ The ``base`` argument to the task decorator specifies the base class of the task
def on_failure(self, exc, task_id, args, kwargs, einfo):
print('{0!r} failed: {1!r}'.format(task_id, exc))
- @task(base=MyTask)
+ @app.task(base=MyTask)
def add(x, y):
raise KeyError()
@@ -318,7 +318,7 @@ on the automatic naming:
.. code-block:: python
- @task(name='proj.tasks.add')
+ @app.task(name='proj.tasks.add')
def add(x, y):
return x + y
From 3328977202c0c1f2b23d21f5ca452595c3a58199 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Sun, 2 May 2021 13:03:32 +0300
Subject: [PATCH 220/415] =?UTF-8?q?Bump=20version:=205.1.0b1=20=E2=86=92?=
=?UTF-8?q?=205.1.0b2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.bumpversion.cfg | 2 +-
README.rst | 6 +++---
celery/__init__.py | 2 +-
docs/includes/introduction.txt | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 3415054d468..057a348b7bb 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 5.1.0b1
+current_version = 5.1.0b2
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z\d]+)?
diff --git a/README.rst b/README.rst
index 7fc9498920a..f4fe61aea17 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
|build-status| |coverage| |license| |wheel| |pyversion| |pyimp| |ocbackerbadge| |ocsponsorbadge|
-:Version: 5.1.0b1 (singularity)
+:Version: 5.1.0b2 (singularity)
:Web: https://docs.celeryproject.org/en/stable/index.html
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
@@ -57,7 +57,7 @@ in such a way that the client enqueues an URL to be requested by a worker.
What do I need?
===============
-Celery version 5.1.0b1 runs on,
+Celery version 5.1.0b2 runs on,
- Python (3.6, 3.7, 3.8, 3.9)
- PyPy3.6 (7.6)
@@ -89,7 +89,7 @@ Get Started
===========
If this is the first time you're trying to use Celery, or you're
-new to Celery 5.0.5 or 5.1.0b1 coming from previous versions then you should read our
+new to Celery 5.0.5 or 5.1.0b2 coming from previous versions then you should read our
getting started tutorials:
- `First steps with Celery`_
diff --git a/celery/__init__.py b/celery/__init__.py
index 898c0138add..a5f7f2f49a5 100644
--- a/celery/__init__.py
+++ b/celery/__init__.py
@@ -17,7 +17,7 @@
SERIES = 'singularity'
-__version__ = '5.1.0b1'
+__version__ = '5.1.0b2'
__author__ = 'Ask Solem'
__contact__ = 'auvipy@gmail.com'
__homepage__ = 'http://celeryproject.org'
diff --git a/docs/includes/introduction.txt b/docs/includes/introduction.txt
index 2f47543eb00..2f395a1fcc6 100644
--- a/docs/includes/introduction.txt
+++ b/docs/includes/introduction.txt
@@ -1,4 +1,4 @@
-:Version: 5.1.0b1 (cliffs)
+:Version: 5.1.0b2 (cliffs)
:Web: http://celeryproject.org/
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
From 55a6324e54c3c181fde9a16dd50fe260cd0cf2e2 Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Thu, 6 May 2021 16:43:54 +0300
Subject: [PATCH 221/415] Added the changelog for 5.1.0b2.
---
Changelog.rst | 181 +++++----------------------------
docs/history/changelog-5.0.rst | 156 ++++++++++++++++++++++++++++
2 files changed, 182 insertions(+), 155 deletions(-)
create mode 100644 docs/history/changelog-5.0.rst
diff --git a/Changelog.rst b/Changelog.rst
index cafe66d43ad..026ed077fb2 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -5,10 +5,31 @@
================
This document contains change notes for bugfix & new features
-in the 5.0.x & 5.1.x series, please see :ref:`whatsnew-5.0` for
-an overview of what's new in Celery 5.0. 5.1.0b1 is an incremental
-pre release with lots of bug fixes and some new features/enhancements.
-Some dependencies were upgraded to newer versions.
+in the & 5.1.x series, please see :ref:`whatsnew-5.1` for
+an overview of what's new in Celery 5.1.
+
+.. _version-5.1.0b2:
+
+5.1.0b2
+=======
+:release-date: 2021-05-02 16.06 P.M UTC+3:00
+:release-by: Omer Katz
+
+- Fix the behavior of our json serialization which regressed in 5.0. (#6561)
+- Add support for SQLAlchemy 1.4. (#6709)
+- Safeguard against schedule entry without kwargs. (#6619)
+- ``task.apply_async(ignore_result=True)`` now avoids persisting the results. (#6713)
+- Update systemd tmpfiles path. (#6688)
+- Ensure AMQPContext exposes an app attribute. (#6741)
+- Inspect commands accept arguments again (#6710).
+- Chord counting of group children is now accurate. (#6733)
+- Add a setting :setting:`worker_cancel_long_running_tasks_on_connection_loss`
+ to terminate tasks with late acknowledgement on connection loss. (#6654)
+- The ``task-revoked`` event and the ``task_revoked` signal are not duplicated
+ when ``Request.on_failure`` is called. (#6654)
+- Restore pickling support for ``Retry``. (#6748)
+- Add support in the redis result backend for authenticating with a username. (#6750)
+- The :setting:`worker_pool` setting is now respected correctly. (#6711)
.. _version-5.1.0b1:
@@ -60,154 +81,4 @@ Some dependencies were upgraded to newer versions.
- Moved CI to github action.
- Updated deployment scripts.
- Updated docker.
-- Initial support of python 3.9 added.
-
-
-.. _version-5.0.5:
-
-5.0.5
-=====
-:release-date: 2020-12-16 5.35 P.M UTC+2:00
-:release-by: Omer Katz
-
-- Ensure keys are strings when deleting results from S3 (#6537).
-- Fix a regression breaking `celery --help` and `celery events` (#6543).
-
-.. _version-5.0.4:
-
-5.0.4
-=====
-:release-date: 2020-12-08 2.40 P.M UTC+2:00
-:release-by: Omer Katz
-
-- DummyClient of cache+memory:// backend now shares state between threads (#6524).
-
- This fixes a problem when using our pytest integration with the in memory
- result backend.
- Because the state wasn't shared between threads, #6416 results in test suites
- hanging on `result.get()`.
-
-.. _version-5.0.3:
-
-5.0.3
-=====
-:release-date: 2020-12-03 6.30 P.M UTC+2:00
-:release-by: Omer Katz
-
-- Make `--workdir` eager for early handling (#6457).
-- When using the MongoDB backend, don't cleanup if result_expires is 0 or None (#6462).
-- Fix passing queues into purge command (#6469).
-- Restore `app.start()` and `app.worker_main()` (#6481).
-- Detaching no longer creates an extra log file (#6426).
-- Result backend instances are now thread local to ensure thread safety (#6416).
-- Don't upgrade click to 8.x since click-repl doesn't support it yet.
-- Restore preload options (#6516).
-
-.. _version-5.0.2:
-
-5.0.2
-=====
-:release-date: 2020-11-02 8.00 P.M UTC+2:00
-:release-by: Omer Katz
-
-- Fix _autodiscover_tasks_from_fixups (#6424).
-- Flush worker prints, notably the banner (#6432).
-- **Breaking Change**: Remove `ha_policy` from queue definition. (#6440)
-
- This argument has no effect since RabbitMQ 3.0.
- Therefore, We feel comfortable dropping it in a patch release.
-
-- Python 3.9 support (#6418).
-- **Regression**: When using the prefork pool, pick the fair scheduling strategy by default (#6447).
-- Preserve callbacks when replacing a task with a chain (#6189).
-- Fix max_retries override on `self.retry()` (#6436).
-- Raise proper error when replacing with an empty chain (#6452)
-
-.. _version-5.0.1:
-
-5.0.1
-=====
-:release-date: 2020-10-18 1.00 P.M UTC+3:00
-:release-by: Omer Katz
-
-- Specify UTF-8 as the encoding for log files (#6357).
-- Custom headers now propagate when using the protocol 1 hybrid messages (#6374).
-- Retry creating the database schema for the database results backend
- in case of a race condition (#6298).
-- When using the Redis results backend, awaiting for a chord no longer hangs
- when setting :setting:`result_expires` to 0 (#6373).
-- When a user tries to specify the app as an option for the subcommand,
- a custom error message is displayed (#6363).
-- Fix the `--without-gossip`, `--without-mingle`, and `--without-heartbeat`
- options which now work as expected. (#6365)
-- Provide a clearer error message when the application cannot be loaded.
-- Avoid printing deprecation warnings for settings when they are loaded from
- Django settings (#6385).
-- Allow lowercase log levels for the `--loglevel` option (#6388).
-- Detaching now works as expected (#6401).
-- Restore broadcasting messages from `celery control` (#6400).
-- Pass back real result for single task chains (#6411).
-- Ensure group tasks a deeply serialized (#6342).
-- Fix chord element counting (#6354).
-- Restore the `celery shell` command (#6421).
-
-.. _version-5.0.0:
-
-5.0.0
-=====
-:release-date: 2020-09-24 6.00 P.M UTC+3:00
-:release-by: Omer Katz
-
-- **Breaking Change** Remove AMQP result backend (#6360).
-- Warn when deprecated settings are used (#6353).
-- Expose retry_policy for Redis result backend (#6330).
-- Prepare Celery to support the yet to be released Python 3.9 (#6328).
-
-5.0.0rc3
-========
-:release-date: 2020-09-07 4.00 P.M UTC+3:00
-:release-by: Omer Katz
-
-- More cleanups of leftover Python 2 support (#6338).
-
-5.0.0rc2
-========
-:release-date: 2020-09-01 6.30 P.M UTC+3:00
-:release-by: Omer Katz
-
-- Bump minimum required eventlet version to 0.26.1.
-- Update Couchbase Result backend to use SDK V3.
-- Restore monkeypatching when gevent or eventlet are used.
-
-5.0.0rc1
-========
-:release-date: 2020-08-24 9.00 P.M UTC+3:00
-:release-by: Omer Katz
-
-- Allow to opt out of ordered group results when using the Redis result backend (#6290).
-- **Breaking Change** Remove the deprecated celery.utils.encoding module.
-
-5.0.0b1
-=======
-:release-date: 2020-08-19 8.30 P.M UTC+3:00
-:release-by: Omer Katz
-
-- **Breaking Change** Drop support for the Riak result backend (#5686).
-- **Breaking Change** pytest plugin is no longer enabled by default (#6288).
- Install pytest-celery to enable it.
-- **Breaking Change** Brand new CLI based on Click (#5718).
-
-5.0.0a2
-=======
-:release-date: 2020-08-05 7.15 P.M UTC+3:00
-:release-by: Omer Katz
-
-- Bump Kombu version to 5.0 (#5686).
-
-5.0.0a1
-=======
-:release-date: 2020-08-02 9.30 P.M UTC+3:00
-:release-by: Omer Katz
-
-- Removed most of the compatibility code that supports Python 2 (#5686).
-- Modernized code to work on Python 3.6 and above (#5686).
+- Initial support of python 3.9 added.
diff --git a/docs/history/changelog-5.0.rst b/docs/history/changelog-5.0.rst
new file mode 100644
index 00000000000..79aa5070c55
--- /dev/null
+++ b/docs/history/changelog-5.0.rst
@@ -0,0 +1,156 @@
+================
+ Change history
+================
+
+This document contains change notes for bugfix & new features
+in the 5.0.x , please see :ref:`whatsnew-5.0` for
+an overview of what's new in Celery 5.0.
+
+.. _version-5.0.5:
+
+5.0.5
+=====
+:release-date: 2020-12-16 5.35 P.M UTC+2:00
+:release-by: Omer Katz
+
+- Ensure keys are strings when deleting results from S3 (#6537).
+- Fix a regression breaking `celery --help` and `celery events` (#6543).
+
+.. _version-5.0.4:
+
+5.0.4
+=====
+:release-date: 2020-12-08 2.40 P.M UTC+2:00
+:release-by: Omer Katz
+
+- DummyClient of cache+memory:// backend now shares state between threads (#6524).
+
+ This fixes a problem when using our pytest integration with the in memory
+ result backend.
+ Because the state wasn't shared between threads, #6416 results in test suites
+ hanging on `result.get()`.
+
+.. _version-5.0.3:
+
+5.0.3
+=====
+:release-date: 2020-12-03 6.30 P.M UTC+2:00
+:release-by: Omer Katz
+
+- Make `--workdir` eager for early handling (#6457).
+- When using the MongoDB backend, don't cleanup if result_expires is 0 or None (#6462).
+- Fix passing queues into purge command (#6469).
+- Restore `app.start()` and `app.worker_main()` (#6481).
+- Detaching no longer creates an extra log file (#6426).
+- Result backend instances are now thread local to ensure thread safety (#6416).
+- Don't upgrade click to 8.x since click-repl doesn't support it yet.
+- Restore preload options (#6516).
+
+.. _version-5.0.2:
+
+5.0.2
+=====
+:release-date: 2020-11-02 8.00 P.M UTC+2:00
+:release-by: Omer Katz
+
+- Fix _autodiscover_tasks_from_fixups (#6424).
+- Flush worker prints, notably the banner (#6432).
+- **Breaking Change**: Remove `ha_policy` from queue definition. (#6440)
+
+ This argument has no effect since RabbitMQ 3.0.
+ Therefore, We feel comfortable dropping it in a patch release.
+
+- Python 3.9 support (#6418).
+- **Regression**: When using the prefork pool, pick the fair scheduling strategy by default (#6447).
+- Preserve callbacks when replacing a task with a chain (#6189).
+- Fix max_retries override on `self.retry()` (#6436).
+- Raise proper error when replacing with an empty chain (#6452)
+
+.. _version-5.0.1:
+
+5.0.1
+=====
+:release-date: 2020-10-18 1.00 P.M UTC+3:00
+:release-by: Omer Katz
+
+- Specify UTF-8 as the encoding for log files (#6357).
+- Custom headers now propagate when using the protocol 1 hybrid messages (#6374).
+- Retry creating the database schema for the database results backend
+ in case of a race condition (#6298).
+- When using the Redis results backend, awaiting for a chord no longer hangs
+ when setting :setting:`result_expires` to 0 (#6373).
+- When a user tries to specify the app as an option for the subcommand,
+ a custom error message is displayed (#6363).
+- Fix the `--without-gossip`, `--without-mingle`, and `--without-heartbeat`
+ options which now work as expected. (#6365)
+- Provide a clearer error message when the application cannot be loaded.
+- Avoid printing deprecation warnings for settings when they are loaded from
+ Django settings (#6385).
+- Allow lowercase log levels for the `--loglevel` option (#6388).
+- Detaching now works as expected (#6401).
+- Restore broadcasting messages from `celery control` (#6400).
+- Pass back real result for single task chains (#6411).
+- Ensure group tasks a deeply serialized (#6342).
+- Fix chord element counting (#6354).
+- Restore the `celery shell` command (#6421).
+
+.. _version-5.0.0:
+
+5.0.0
+=====
+:release-date: 2020-09-24 6.00 P.M UTC+3:00
+:release-by: Omer Katz
+
+- **Breaking Change** Remove AMQP result backend (#6360).
+- Warn when deprecated settings are used (#6353).
+- Expose retry_policy for Redis result backend (#6330).
+- Prepare Celery to support the yet to be released Python 3.9 (#6328).
+
+5.0.0rc3
+========
+:release-date: 2020-09-07 4.00 P.M UTC+3:00
+:release-by: Omer Katz
+
+- More cleanups of leftover Python 2 support (#6338).
+
+5.0.0rc2
+========
+:release-date: 2020-09-01 6.30 P.M UTC+3:00
+:release-by: Omer Katz
+
+- Bump minimum required eventlet version to 0.26.1.
+- Update Couchbase Result backend to use SDK V3.
+- Restore monkeypatching when gevent or eventlet are used.
+
+5.0.0rc1
+========
+:release-date: 2020-08-24 9.00 P.M UTC+3:00
+:release-by: Omer Katz
+
+- Allow to opt out of ordered group results when using the Redis result backend (#6290).
+- **Breaking Change** Remove the deprecated celery.utils.encoding module.
+
+5.0.0b1
+=======
+:release-date: 2020-08-19 8.30 P.M UTC+3:00
+:release-by: Omer Katz
+
+- **Breaking Change** Drop support for the Riak result backend (#5686).
+- **Breaking Change** pytest plugin is no longer enabled by default (#6288).
+ Install pytest-celery to enable it.
+- **Breaking Change** Brand new CLI based on Click (#5718).
+
+5.0.0a2
+=======
+:release-date: 2020-08-05 7.15 P.M UTC+3:00
+:release-by: Omer Katz
+
+- Bump Kombu version to 5.0 (#5686).
+
+5.0.0a1
+=======
+:release-date: 2020-08-02 9.30 P.M UTC+3:00
+:release-by: Omer Katz
+
+- Removed most of the compatibility code that supports Python 2 (#5686).
+- Modernized code to work on Python 3.6 and above (#5686).
From 426a8f97e9f7dd19905ec624182b6d4a61bc245e Mon Sep 17 00:00:00 2001
From: Matus Valo
Date: Thu, 6 May 2021 15:47:54 +0200
Subject: [PATCH 222/415] Celery Mailbox accept and serializer parameters are
initialized from configuration (#6757)
---
celery/app/control.py | 3 ++-
t/unit/app/test_control.py | 6 ++++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/celery/app/control.py b/celery/app/control.py
index a35f5cec246..05b7012ac3d 100644
--- a/celery/app/control.py
+++ b/celery/app/control.py
@@ -431,7 +431,8 @@ def __init__(self, app=None):
self.mailbox = self.Mailbox(
app.conf.control_exchange,
type='fanout',
- accept=['json'],
+ accept=app.conf.accept_content,
+ serializer=app.conf.task_serializer,
producer_pool=lazy(lambda: self.app.amqp.producer_pool),
queue_ttl=app.conf.control_queue_ttl,
reply_queue_ttl=app.conf.control_queue_ttl,
diff --git a/t/unit/app/test_control.py b/t/unit/app/test_control.py
index 5757af757b0..2a80138c09b 100644
--- a/t/unit/app/test_control.py
+++ b/t/unit/app/test_control.py
@@ -241,6 +241,12 @@ def assert_control_called_with_args(self, name, destination=None,
self.app.control.broadcast.assert_called_with(
name, destination=destination, arguments=args, **_options or {})
+ def test_serializer(self):
+ self.app.conf['task_serializer'] = 'test'
+ self.app.conf['accept_content'] = ['test']
+ assert control.Control(self.app).mailbox.serializer == 'test'
+ assert control.Control(self.app).mailbox.accept == ['test']
+
def test_purge(self):
self.app.amqp.TaskConsumer = Mock(name='TaskConsumer')
self.app.control.purge()
From 6dd385258297c89843bfe73299e5f7eebf0e98e2 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Sat, 8 May 2021 22:19:27 +1000
Subject: [PATCH 223/415] fix: Error propagation and errback calling for
group-like signatures (#6746)
* fix: Use chord kwarg over request in group.apply
* fix: Propagate errors from failed chain tasks
Fixes #6220
Co-authored-by: Maximilian Friedersdorff
Co-authored-by: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
* fix: Ensure all subtasks of a group get errbacks
Giving a linked task callback to the 0th task in a group is fine, but
for errbacks it's not an appropriate choice since any task in the group
could fail. This ensures that if any task other than the 0th one fails,
the errback will be called. This opens the possibility for an errback to
be called more than once when linked to a group, but generally we expect
that they should be design to be idempotent so no warning is issued for
the changed behaviour.
* test: Add tests for child error propagation
* test: Add regression tests for group errback dupes
These tests simply encode the currently expected behaviour where
errbacks linked to a group will be called once for each failed task, as
well as the consequences for chords which turn their header into a group
if it is not one already.
* doc: Add extra docs for canvas call/errback usage
Co-authored-by: Crawford, Jordan
Co-authored-by: Maximilian Friedersdorff
---
celery/backends/base.py | 37 +++
celery/canvas.py | 11 +-
docs/userguide/canvas.rst | 52 +++++
t/integration/tasks.py | 7 +
t/integration/test_canvas.py | 436 ++++++++++++++++++++++++++++++++++-
t/unit/tasks/test_canvas.py | 2 +-
6 files changed, 541 insertions(+), 4 deletions(-)
diff --git a/celery/backends/base.py b/celery/backends/base.py
index fdec6d58f46..7d4fbbdc3b7 100644
--- a/celery/backends/base.py
+++ b/celery/backends/base.py
@@ -22,6 +22,7 @@
import celery.exceptions
from celery import current_app, group, maybe_signature, states
from celery._state import get_current_task
+from celery.app.task import Context
from celery.exceptions import (BackendGetMetaError, BackendStoreError,
ChordError, ImproperlyConfigured,
NotRegistered, TaskRevokedError, TimeoutError)
@@ -170,8 +171,44 @@ def mark_as_failure(self, task_id, exc,
self.store_result(task_id, exc, state,
traceback=traceback, request=request)
if request:
+ # This task may be part of a chord
if request.chord:
self.on_chord_part_return(request, state, exc)
+ # It might also have chained tasks which need to be propagated to,
+ # this is most likely to be exclusive with being a direct part of a
+ # chord but we'll handle both cases separately.
+ #
+ # The `chain_data` try block here is a bit tortured since we might
+ # have non-iterable objects here in tests and it's easier this way.
+ try:
+ chain_data = iter(request.chain)
+ except (AttributeError, TypeError):
+ chain_data = tuple()
+ for chain_elem in chain_data:
+ chain_elem_opts = chain_elem['options']
+ # If the state should be propagated, we'll do so for all
+ # elements of the chain. This is only truly important so
+ # that the last chain element which controls completion of
+ # the chain itself is marked as completed to avoid stalls.
+ if self.store_result and state in states.PROPAGATE_STATES:
+ try:
+ chained_task_id = chain_elem_opts['task_id']
+ except KeyError:
+ pass
+ else:
+ self.store_result(
+ chained_task_id, exc, state,
+ traceback=traceback, request=chain_elem
+ )
+ # If the chain element is a member of a chord, we also need
+ # to call `on_chord_part_return()` as well to avoid stalls.
+ if 'chord' in chain_elem_opts:
+ failed_ctx = Context(chain_elem)
+ failed_ctx.update(failed_ctx.options)
+ failed_ctx.id = failed_ctx.options['task_id']
+ failed_ctx.group = failed_ctx.options['group_id']
+ self.on_chord_part_return(failed_ctx, state, exc)
+ # And finally we'll fire any errbacks
if call_errbacks and request.errbacks:
self._call_task_errbacks(request, exc, traceback)
diff --git a/celery/canvas.py b/celery/canvas.py
index a80e979af96..9b32e832fd0 100644
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -1134,7 +1134,14 @@ def link_error(self, sig):
# pass a Mock object as argument.
sig['immutable'] = True
sig = Signature.from_dict(sig)
- return self.tasks[0].link_error(sig)
+ # Any child task might error so we need to ensure that they are all
+ # capable of calling the linked error signature. This opens the
+ # possibility that the task is called more than once but that's better
+ # than it not being called at all.
+ #
+ # We return a concretised tuple of the signatures actually applied to
+ # each child task signature, of which there might be none!
+ return tuple(child_task.link_error(sig) for child_task in self.tasks)
def _prepared(self, tasks, partial_args, group_id, root_id, app,
CallableSignature=abstract.CallableSignature,
@@ -1179,7 +1186,7 @@ def _apply_tasks(self, tasks, producer=None, app=None, p=None,
# end up messing up chord counts and there are all sorts of
# awful race conditions to think about. We'll hope it's not!
sig, res, group_id = current_task
- chord_obj = sig.options.get("chord") or chord
+ chord_obj = chord if chord is not None else sig.options.get("chord")
# We need to check the chord size of each contributing task so
# that when we get to the final one, we can correctly set the
# size in the backend and the chord can be sensible completed.
diff --git a/docs/userguide/canvas.rst b/docs/userguide/canvas.rst
index 55811f2fbe0..45912a6d2c9 100644
--- a/docs/userguide/canvas.rst
+++ b/docs/userguide/canvas.rst
@@ -688,6 +688,52 @@ Group also supports iterators:
A group is a signature object, so it can be used in combination
with other signatures.
+.. _group-callbacks:
+
+Group Callbacks and Error Handling
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Groups can have callback and errback signatures linked to them as well, however
+the behaviour can be somewhat surprising due to the fact that groups are not
+real tasks and simply pass linked tasks down to their encapsulated signatures.
+This means that the return values of a group are not collected to be passed to
+a linked callback signature.
+As an example, the following snippet using a simple `add(a, b)` task is faulty
+since the linked `add.s()` signature will not received the finalised group
+result as one might expect.
+
+.. code-block:: pycon
+
+ >>> g = group(add.s(2, 2), add.s(4, 4))
+ >>> g.link(add.s())
+ >>> res = g()
+ [4, 8]
+
+Note that the finalised results of the first two tasks are returned, but the
+callback signature will have run in the background and raised an exception
+since it did not receive the two arguments it expects.
+
+Group errbacks are passed down to encapsulated signatures as well which opens
+the possibility for an errback linked only once to be called more than once if
+multiple tasks in a group were to fail.
+As an example, the following snippet using a `fail()` task which raises an
+exception can be expected to invoke the `log_error()` signature once for each
+failing task which gets run in the group.
+
+.. code-block:: pycon
+
+ >>> g = group(fail.s(), fail.s())
+ >>> g.link_error(log_error.s())
+ >>> res = g()
+
+With this in mind, it's generally advisable to create idempotent or counting
+tasks which are tolerant to being called repeatedly for use as errbacks.
+
+These use cases are better addressed by the :class:`~celery.chord` class which
+is supported on certain backend implementations.
+
+.. _group-results:
+
Group Results
~~~~~~~~~~~~~
@@ -884,6 +930,12 @@ an errback to the chord callback:
>>> c = (group(add.s(i, i) for i in range(10)) |
... xsum.s().on_error(on_chord_error.s())).delay()
+Chords may have callback and errback signatures linked to them, which addresses
+some of the issues with linking signatures to groups.
+Doing so will link the provided signature to the chord's body which can be
+expected to gracefully invoke callbacks just once upon completion of the body,
+or errbacks just once if any task in the chord header or body fails.
+
.. _chord-important-notes:
Important Notes
diff --git a/t/integration/tasks.py b/t/integration/tasks.py
index 4e88bcd880a..d1b825fcf53 100644
--- a/t/integration/tasks.py
+++ b/t/integration/tasks.py
@@ -223,6 +223,13 @@ def redis_echo(message):
redis_connection.rpush('redis-echo', message)
+@shared_task
+def redis_count():
+ """Task that increments a well-known redis key."""
+ redis_connection = get_redis_connection()
+ redis_connection.incr('redis-count')
+
+
@shared_task(bind=True)
def second_order_replace1(self, state=False):
redis_connection = get_redis_connection()
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
index 28560e33e64..02beb8550d4 100644
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -1,9 +1,11 @@
import re
import tempfile
+import uuid
from datetime import datetime, timedelta
from time import sleep
import pytest
+import pytest_subtests # noqa: F401
from celery import chain, chord, group, signature
from celery.backends.base import BaseKeyValueStoreBackend
@@ -17,7 +19,7 @@
add_to_all, add_to_all_to_chord, build_chain_inside_task,
chord_error, collect_ids, delayed_sum,
delayed_sum_with_soft_guard, fail, identity, ids,
- print_unicode, raise_error, redis_echo,
+ print_unicode, raise_error, redis_count, redis_echo,
replace_with_chain, replace_with_chain_which_raises,
replace_with_empty_chain, retry_once, return_exception,
return_priority, second_order_replace1, tsum,
@@ -810,6 +812,109 @@ def test_nested_group_chord_body_chain(self, manager):
# Re-raise the expected exception so this test will XFAIL
raise expected_excinfo.value
+ def test_callback_called_by_group(self, manager, subtests):
+ if not manager.app.conf.result_backend.startswith("redis"):
+ raise pytest.skip("Requires redis result backend.")
+ redis_connection = get_redis_connection()
+
+ callback_msg = str(uuid.uuid4()).encode()
+ callback = redis_echo.si(callback_msg)
+
+ group_sig = group(identity.si(42), identity.si(1337))
+ group_sig.link(callback)
+ redis_connection.delete("redis-echo")
+ with subtests.test(msg="Group result is returned"):
+ res = group_sig.delay()
+ assert res.get(timeout=TIMEOUT) == [42, 1337]
+ with subtests.test(msg="Callback is called after group is completed"):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Callback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == callback_msg
+
+ def test_errback_called_by_group_fail_first(self, manager, subtests):
+ if not manager.app.conf.result_backend.startswith("redis"):
+ raise pytest.skip("Requires redis result backend.")
+ redis_connection = get_redis_connection()
+
+ errback_msg = str(uuid.uuid4()).encode()
+ errback = redis_echo.si(errback_msg)
+
+ group_sig = group(fail.s(), identity.si(42))
+ group_sig.link_error(errback)
+ redis_connection.delete("redis-echo")
+ with subtests.test(msg="Error propagates from group"):
+ res = group_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(msg="Errback is called after group task fails"):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Errback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == errback_msg
+
+ def test_errback_called_by_group_fail_last(self, manager, subtests):
+ if not manager.app.conf.result_backend.startswith("redis"):
+ raise pytest.skip("Requires redis result backend.")
+ redis_connection = get_redis_connection()
+
+ errback_msg = str(uuid.uuid4()).encode()
+ errback = redis_echo.si(errback_msg)
+
+ group_sig = group(identity.si(42), fail.s())
+ group_sig.link_error(errback)
+ redis_connection.delete("redis-echo")
+ with subtests.test(msg="Error propagates from group"):
+ res = group_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(msg="Errback is called after group task fails"):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Errback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == errback_msg
+
+ def test_errback_called_by_group_fail_multiple(self, manager, subtests):
+ if not manager.app.conf.result_backend.startswith("redis"):
+ raise pytest.skip("Requires redis result backend.")
+ redis_connection = get_redis_connection()
+
+ expected_errback_count = 42
+ errback = redis_count.si()
+
+ # Include a mix of passing and failing tasks
+ group_sig = group(
+ *(identity.si(42) for _ in range(24)), # arbitrary task count
+ *(fail.s() for _ in range(expected_errback_count)),
+ )
+ group_sig.link_error(errback)
+ redis_connection.delete("redis-count")
+ with subtests.test(msg="Error propagates from group"):
+ res = group_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(msg="Errback is called after group task fails"):
+ check_interval = 0.1
+ check_max = int(TIMEOUT * check_interval)
+ for i in range(check_max + 1):
+ maybe_count = redis_connection.get("redis-count")
+ # It's either `None` or a base-10 integer
+ count = int(maybe_count or b"0")
+ if count == expected_errback_count:
+ # escape and pass
+ break
+ elif i < check_max:
+ # try again later
+ sleep(check_interval)
+ else:
+ # fail
+ assert count == expected_errback_count
+ else:
+ raise TimeoutError("Errbacks were not called in time")
+
def assert_ids(r, expected_value, expected_root_id, expected_parent_id):
root_id, parent_id, value = r.get(timeout=TIMEOUT)
@@ -1406,6 +1511,335 @@ def test_error_propagates_from_chord2(self, manager):
with pytest.raises(ExpectedException):
res.get(timeout=TIMEOUT)
+ def test_error_propagates_to_chord_from_simple(self, manager, subtests):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ child_sig = fail.s()
+
+ chord_sig = chord((child_sig, ), identity.s())
+ with subtests.test(msg="Error propagates from simple header task"):
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+
+ chord_sig = chord((identity.si(42), ), child_sig)
+ with subtests.test(msg="Error propagates from simple body task"):
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+
+ def test_errback_called_by_chord_from_simple(self, manager, subtests):
+ if not manager.app.conf.result_backend.startswith("redis"):
+ raise pytest.skip("Requires redis result backend.")
+ redis_connection = get_redis_connection()
+
+ errback_msg = str(uuid.uuid4()).encode()
+ errback = redis_echo.si(errback_msg)
+ child_sig = fail.s()
+
+ chord_sig = chord((child_sig, ), identity.s())
+ chord_sig.link_error(errback)
+ with subtests.test(msg="Error propagates from simple header task"):
+ redis_connection.delete("redis-echo")
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(
+ msg="Errback is called after simple header task fails"
+ ):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Errback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == errback_msg
+
+ chord_sig = chord((identity.si(42), ), child_sig)
+ chord_sig.link_error(errback)
+ with subtests.test(msg="Error propagates from simple body task"):
+ redis_connection.delete("redis-echo")
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(
+ msg="Errback is called after simple body task fails"
+ ):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Errback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == errback_msg
+
+ def test_error_propagates_to_chord_from_chain(self, manager, subtests):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ child_sig = chain(identity.si(42), fail.s(), identity.si(42))
+
+ chord_sig = chord((child_sig, ), identity.s())
+ with subtests.test(
+ msg="Error propagates from header chain which fails before the end"
+ ):
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+
+ chord_sig = chord((identity.si(42), ), child_sig)
+ with subtests.test(
+ msg="Error propagates from body chain which fails before the end"
+ ):
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+
+ def test_errback_called_by_chord_from_chain(self, manager, subtests):
+ if not manager.app.conf.result_backend.startswith("redis"):
+ raise pytest.skip("Requires redis result backend.")
+ redis_connection = get_redis_connection()
+
+ errback_msg = str(uuid.uuid4()).encode()
+ errback = redis_echo.si(errback_msg)
+ child_sig = chain(identity.si(42), fail.s(), identity.si(42))
+
+ chord_sig = chord((child_sig, ), identity.s())
+ chord_sig.link_error(errback)
+ with subtests.test(
+ msg="Error propagates from header chain which fails before the end"
+ ):
+ redis_connection.delete("redis-echo")
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(
+ msg="Errback is called after header chain which fails before the end"
+ ):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Errback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == errback_msg
+
+ chord_sig = chord((identity.si(42), ), child_sig)
+ chord_sig.link_error(errback)
+ with subtests.test(
+ msg="Error propagates from body chain which fails before the end"
+ ):
+ redis_connection.delete("redis-echo")
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(
+ msg="Errback is called after body chain which fails before the end"
+ ):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Errback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == errback_msg
+
+ def test_error_propagates_to_chord_from_chain_tail(self, manager, subtests):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ child_sig = chain(identity.si(42), fail.s())
+
+ chord_sig = chord((child_sig, ), identity.s())
+ with subtests.test(
+ msg="Error propagates from header chain which fails at the end"
+ ):
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+
+ chord_sig = chord((identity.si(42), ), child_sig)
+ with subtests.test(
+ msg="Error propagates from body chain which fails at the end"
+ ):
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+
+ def test_errback_called_by_chord_from_chain_tail(self, manager, subtests):
+ if not manager.app.conf.result_backend.startswith("redis"):
+ raise pytest.skip("Requires redis result backend.")
+ redis_connection = get_redis_connection()
+
+ errback_msg = str(uuid.uuid4()).encode()
+ errback = redis_echo.si(errback_msg)
+ child_sig = chain(identity.si(42), fail.s())
+
+ chord_sig = chord((child_sig, ), identity.s())
+ chord_sig.link_error(errback)
+ with subtests.test(
+ msg="Error propagates from header chain which fails at the end"
+ ):
+ redis_connection.delete("redis-echo")
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(
+ msg="Errback is called after header chain which fails at the end"
+ ):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Errback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == errback_msg
+
+ chord_sig = chord((identity.si(42), ), child_sig)
+ chord_sig.link_error(errback)
+ with subtests.test(
+ msg="Error propagates from body chain which fails at the end"
+ ):
+ redis_connection.delete("redis-echo")
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(
+ msg="Errback is called after body chain which fails at the end"
+ ):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Errback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == errback_msg
+
+ def test_error_propagates_to_chord_from_group(self, manager, subtests):
+ try:
+ manager.app.backend.ensure_chords_allowed()
+ except NotImplementedError as e:
+ raise pytest.skip(e.args[0])
+
+ child_sig = group(identity.si(42), fail.s())
+
+ chord_sig = chord((child_sig, ), identity.s())
+ with subtests.test(msg="Error propagates from header group"):
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+
+ chord_sig = chord((identity.si(42), ), child_sig)
+ with subtests.test(msg="Error propagates from body group"):
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+
+ def test_errback_called_by_chord_from_group(self, manager, subtests):
+ if not manager.app.conf.result_backend.startswith("redis"):
+ raise pytest.skip("Requires redis result backend.")
+ redis_connection = get_redis_connection()
+
+ errback_msg = str(uuid.uuid4()).encode()
+ errback = redis_echo.si(errback_msg)
+ child_sig = group(identity.si(42), fail.s())
+
+ chord_sig = chord((child_sig, ), identity.s())
+ chord_sig.link_error(errback)
+ with subtests.test(msg="Error propagates from header group"):
+ redis_connection.delete("redis-echo")
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(msg="Errback is called after header group fails"):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Errback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == errback_msg
+
+ chord_sig = chord((identity.si(42), ), child_sig)
+ chord_sig.link_error(errback)
+ with subtests.test(msg="Error propagates from body group"):
+ redis_connection.delete("redis-echo")
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(msg="Errback is called after body group fails"):
+ maybe_key_msg = redis_connection.blpop("redis-echo", TIMEOUT)
+ if maybe_key_msg is None:
+ raise TimeoutError("Errback was not called in time")
+ _, msg = maybe_key_msg
+ assert msg == errback_msg
+
+ def test_errback_called_by_chord_from_group_fail_multiple(
+ self, manager, subtests
+ ):
+ if not manager.app.conf.result_backend.startswith("redis"):
+ raise pytest.skip("Requires redis result backend.")
+ redis_connection = get_redis_connection()
+
+ fail_task_count = 42
+ errback = redis_count.si()
+ # Include a mix of passing and failing tasks
+ child_sig = group(
+ *(identity.si(42) for _ in range(24)), # arbitrary task count
+ *(fail.s() for _ in range(fail_task_count)),
+ )
+
+ chord_sig = chord((child_sig, ), identity.s())
+ chord_sig.link_error(errback)
+ with subtests.test(msg="Error propagates from header group"):
+ redis_connection.delete("redis-count")
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(msg="Errback is called after header group fails"):
+ # NOTE: Here we only expect the errback to be called once since it
+ # is attached to the chord body which is a single task!
+ expected_errback_count = 1
+ check_interval = 0.1
+ check_max = int(TIMEOUT * check_interval)
+ for i in range(check_max + 1):
+ maybe_count = redis_connection.get("redis-count")
+ # It's either `None` or a base-10 integer
+ count = int(maybe_count or b"0")
+ if count == expected_errback_count:
+ # escape and pass
+ break
+ elif i < check_max:
+ # try again later
+ sleep(check_interval)
+ else:
+ # fail
+ assert count == expected_errback_count
+ else:
+ raise TimeoutError("Errbacks were not called in time")
+
+ chord_sig = chord((identity.si(42), ), child_sig)
+ chord_sig.link_error(errback)
+ with subtests.test(msg="Error propagates from body group"):
+ redis_connection.delete("redis-count")
+ res = chord_sig.delay()
+ with pytest.raises(ExpectedException):
+ res.get(timeout=TIMEOUT)
+ with subtests.test(msg="Errback is called after body group fails"):
+ # NOTE: Here we expect the errback to be called once per failing
+ # task in the chord body since it is a group
+ expected_errback_count = fail_task_count
+ check_interval = 0.1
+ check_max = int(TIMEOUT * check_interval)
+ for i in range(check_max + 1):
+ maybe_count = redis_connection.get("redis-count")
+ # It's either `None` or a base-10 integer
+ count = int(maybe_count or b"0")
+ if count == expected_errback_count:
+ # escape and pass
+ break
+ elif i < check_max:
+ # try again later
+ sleep(check_interval)
+ else:
+ # fail
+ assert count == expected_errback_count
+ else:
+ raise TimeoutError("Errbacks were not called in time")
+
class test_signature_serialization:
"""
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
index c6e9ca86035..7527f0aed24 100644
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -1,5 +1,5 @@
import json
-from unittest.mock import MagicMock, Mock, call, patch, sentinel, ANY
+from unittest.mock import ANY, MagicMock, Mock, call, patch, sentinel
import pytest
import pytest_subtests # noqa: F401
From 4c12c45e6552b2ec6423d6458684e14dd182260f Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 10 May 2021 15:51:35 +0300
Subject: [PATCH 224/415] Update badge to Github Actions.
---
README.rst | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.rst b/README.rst
index f4fe61aea17..8e01787c437 100644
--- a/README.rst
+++ b/README.rst
@@ -117,7 +117,7 @@ Celery is...
like at our `mailing-list`_, or the IRC channel.
Here's one of the simplest applications you can make:
-
+
.. code-block:: python
from celery import Celery
@@ -500,9 +500,9 @@ file in the top distribution directory for the full license text.
.. # vim: syntax=rst expandtab tabstop=4 shiftwidth=4 shiftround
-.. |build-status| image:: https://api.travis-ci.com/celery/celery.png?branch=master
+.. |build-status| image:: https://github.com/celery/celery/actions/workflows/python-package.yml/badge.svg
:alt: Build status
- :target: https://travis-ci.com/celery/celery
+ :target: https://github.com/celery/celery/actions/workflows/python-package.yml
.. |coverage| image:: https://codecov.io/github/celery/celery/coverage.svg?branch=master
:target: https://codecov.io/github/celery/celery?branch=master
From 1cd6521344c95ca2ddaa8feffb51b4c6612d740c Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Mon, 10 May 2021 15:52:49 +0300
Subject: [PATCH 225/415] Update badge in release issue templates.
---
.github/ISSUE_TEMPLATE/Major-Version-Release-Checklist.md | 2 +-
.github/ISSUE_TEMPLATE/Minor-Version-Release-Checklist.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/Major-Version-Release-Checklist.md b/.github/ISSUE_TEMPLATE/Major-Version-Release-Checklist.md
index eeecc14df18..20e96f036fd 100644
--- a/.github/ISSUE_TEMPLATE/Major-Version-Release-Checklist.md
+++ b/.github/ISSUE_TEMPLATE/Major-Version-Release-Checklist.md
@@ -18,7 +18,7 @@ Release PR:
- [ ] Release PR reviewed
- [ ] The master branch build passes
- [](https://travis-ci.org/celery/celery)
+ [](https://github.com/celery/celery/actions/workflows/python-package.yml)
- [ ] Release Notes
- [ ] What's New
diff --git a/.github/ISSUE_TEMPLATE/Minor-Version-Release-Checklist.md b/.github/ISSUE_TEMPLATE/Minor-Version-Release-Checklist.md
index 208e34bd77f..c3656043b93 100644
--- a/.github/ISSUE_TEMPLATE/Minor-Version-Release-Checklist.md
+++ b/.github/ISSUE_TEMPLATE/Minor-Version-Release-Checklist.md
@@ -12,7 +12,7 @@ Release PR:
- [ ] Release PR reviewed
- [ ] The master branch build passes
- [](https://travis-ci.org/celery/celery)
+ [](https://github.com/celery/celery/actions/workflows/python-package.yml)
- [ ] Release Notes
- [ ] What's New
From 8ce86711e84895e2bc0be005abc14780c0c7ea86 Mon Sep 17 00:00:00 2001
From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
Date: Wed, 12 May 2021 17:26:54 +1000
Subject: [PATCH 226/415] fix: Sanitization of passwords in sentinel URIs
(#6765)
The kombu helper we use is only capable of parsing and santizing a
single URI, so in order to properly sanitize values for multiple servers
to be used by the Redis Sentinel backend, we need to break the string up
into individual server URIs first.
Fixes #6763
---
celery/backends/redis.py | 29 ++++++++++++++++++++++++++---
t/unit/backends/test_redis.py | 10 ++++++++++
2 files changed, 36 insertions(+), 3 deletions(-)
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
index a52cf33d519..eff0fa3442d 100644
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -7,7 +7,7 @@
from kombu.utils.functional import retry_over_time
from kombu.utils.objects import cached_property
-from kombu.utils.url import _parse_url
+from kombu.utils.url import _parse_url, maybe_sanitize_url
from celery import states
from celery._state import task_join_will_block
@@ -585,6 +585,8 @@ class SentinelManagedSSLConnection(
class SentinelBackend(RedisBackend):
"""Redis sentinel task result store."""
+ # URL looks like `sentinel://0.0.0.0:26347/3;sentinel://0.0.0.0:26348/3`
+ _SERVER_URI_SEPARATOR = ";"
sentinel = getattr(redis, "sentinel", None)
connection_class_ssl = SentinelManagedSSLConnection if sentinel else None
@@ -595,9 +597,30 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ def as_uri(self, include_password=False):
+ """
+ Return the server addresses as URIs, sanitizing the password or not.
+ """
+ # Allow superclass to do work if we don't need to force sanitization
+ if include_password:
+ return super(SentinelBackend, self).as_uri(
+ include_password=include_password,
+ )
+ # Otherwise we need to ensure that all components get sanitized rather
+ # by passing them one by one to the `kombu` helper
+ uri_chunks = (
+ maybe_sanitize_url(chunk)
+ for chunk in (self.url or "").split(self._SERVER_URI_SEPARATOR)
+ )
+ # Similar to the superclass, strip the trailing slash from URIs with
+ # all components empty other than the scheme
+ return self._SERVER_URI_SEPARATOR.join(
+ uri[:-1] if uri.endswith(":///") else uri
+ for uri in uri_chunks
+ )
+
def _params_from_url(self, url, defaults):
- # URL looks like sentinel://0.0.0.0:26347/3;sentinel://0.0.0.0:26348/3.
- chunks = url.split(";")
+ chunks = url.split(self._SERVER_URI_SEPARATOR)
connparams = dict(defaults, hosts=[])
for chunk in chunks:
data = super()._params_from_url(
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
index c96bcca357a..05805733f62 100644
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -1203,6 +1203,16 @@ def test_url(self):
found_dbs = [cp['db'] for cp in x.connparams['hosts']]
assert found_dbs == expected_dbs
+ # By default passwords should be sanitized
+ display_url = x.as_uri()
+ assert "test" not in display_url
+ # We can choose not to sanitize with the `include_password` argument
+ unsanitized_display_url = x.as_uri(include_password=True)
+ assert unsanitized_display_url == x.url
+ # or to explicitly sanitize
+ forcibly_sanitized_display_url = x.as_uri(include_password=False)
+ assert forcibly_sanitized_display_url == display_url
+
def test_get_sentinel_instance(self):
x = self.Backend(
'sentinel://:test@github.com:123/1;'
From 2411504f4164ac9acfa20007038d37591c6f57e5 Mon Sep 17 00:00:00 2001
From: Dave Johansen
Date: Wed, 12 May 2021 21:40:52 -0600
Subject: [PATCH 227/415] Add LOG_RECEIVED to customize logging (#6758)
* Add LOG_RECEIVED to customize logging
Co-authored-by: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
* doc: Add minimal docs for task log fmt overrides
* test: Add tests for `LOG_RECEIVED` messages
* test: Fix ineffective logging disabled test
This test wouldn't actually fail if the line forcing `isEnabledFor` to
return `False` was commented out. This changes the test to use log
capture rather than mocking to ensure we actually catch regressions.
Co-authored-by: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com>
---
celery/app/trace.py | 5 +++++
celery/worker/strategy.py | 6 +++++-
docs/userguide/extending.rst | 26 +++++++++++++++++++++++++
t/unit/worker/test_strategy.py | 35 ++++++++++++++++++++++++++++++++--
4 files changed, 69 insertions(+), 3 deletions(-)
diff --git a/celery/app/trace.py b/celery/app/trace.py
index fb4fdd6d7e5..9a56f870768 100644
--- a/celery/app/trace.py
+++ b/celery/app/trace.py
@@ -52,6 +52,11 @@
logger = get_logger(__name__)
+#: Format string used to log task receipt.
+LOG_RECEIVED = """\
+Task %(name)s[%(id)s] received\
+"""
+
#: Format string used to log task success.
LOG_SUCCESS = """\
Task %(name)s[%(id)s] succeeded in %(runtime)ss: %(return_value)s\
diff --git a/celery/worker/strategy.py b/celery/worker/strategy.py
index 98a47015352..09bdea7c1be 100644
--- a/celery/worker/strategy.py
+++ b/celery/worker/strategy.py
@@ -4,6 +4,7 @@
from kombu.asynchronous.timer import to_timestamp
from celery import signals
+from celery.app import trace as _app_trace
from celery.exceptions import InvalidTaskError
from celery.utils.imports import symbol_by_name
from celery.utils.log import get_logger
@@ -148,7 +149,10 @@ def task_message_handler(message, body, ack, reject, callbacks,
body=body, headers=headers, decoded=decoded, utc=utc,
)
if _does_info:
- info('Received task: %s', req)
+ # Similar to `app.trace.info()`, we pass the formatting args as the
+ # `extra` kwarg for custom log handlers
+ context = {'id': req.id, 'name': req.name}
+ info(_app_trace.LOG_RECEIVED, context, extra={'data': context})
if (req.expires or req.id in revoked_tasks) and req.revoked():
return
diff --git a/docs/userguide/extending.rst b/docs/userguide/extending.rst
index cf3a9929be8..59c8f83401e 100644
--- a/docs/userguide/extending.rst
+++ b/docs/userguide/extending.rst
@@ -301,6 +301,32 @@ Another example could use the timer to wake up at regular intervals:
if req.time_start and time() - req.time_start > self.timeout:
raise SystemExit()
+Customizing Task Handling Logs
+------------------------------
+
+The Celery worker emits messages to the Python logging subsystem for various
+events throughout the lifecycle of a task.
+These messages can be customized by overriding the ``LOG_`` format
+strings which are defined in :file:`celery/app/trace.py`.
+For example:
+
+.. code-block:: python
+
+ import celery.app.trace
+
+ celery.app.trace.LOG_SUCCESS = "This is a custom message"
+
+The various format strings are all provided with the task name and ID for
+``%`` formatting, and some of them receive extra fields like the return value
+or the exception which caused a task to fail.
+These fields can be used in custom format strings like so:
+
+.. code-block:: python
+
+ import celery.app.trace
+
+ celery.app.trace.LOG_REJECTED = "%(name)r is cursed and I won't run it: %(exc)s"
+
.. _extending-consumer_blueprint:
Consumer
diff --git a/t/unit/worker/test_strategy.py b/t/unit/worker/test_strategy.py
index 88abe4dcd27..cb8c73d17cb 100644
--- a/t/unit/worker/test_strategy.py
+++ b/t/unit/worker/test_strategy.py
@@ -1,3 +1,4 @@
+import logging
from collections import defaultdict
from contextlib import contextmanager
from unittest.mock import ANY, Mock, patch
@@ -6,6 +7,7 @@
from kombu.utils.limits import TokenBucket
from celery import Task, signals
+from celery.app.trace import LOG_RECEIVED
from celery.exceptions import InvalidTaskError
from celery.utils.time import rate
from celery.worker import state
@@ -142,12 +144,14 @@ def _context(self, sig,
message = self.prepare_message(message)
yield self.Context(sig, s, reserved, consumer, message)
- def test_when_logging_disabled(self):
+ def test_when_logging_disabled(self, caplog):
+ # Capture logs at any level above `NOTSET`
+ caplog.set_level(logging.NOTSET + 1, logger="celery.worker.strategy")
with patch('celery.worker.strategy.logger') as logger:
logger.isEnabledFor.return_value = False
with self._context(self.add.s(2, 2)) as C:
C()
- logger.info.assert_not_called()
+ assert not caplog.records
def test_task_strategy(self):
with self._context(self.add.s(2, 2)) as C:
@@ -165,6 +169,33 @@ def test_callbacks(self):
for callback in callbacks:
callback.assert_called_with(req)
+ def test_log_task_received(self, caplog):
+ caplog.set_level(logging.INFO, logger="celery.worker.strategy")
+ with self._context(self.add.s(2, 2)) as C:
+ C()
+ for record in caplog.records:
+ if record.msg == LOG_RECEIVED:
+ assert record.levelno == logging.INFO
+ break
+ else:
+ raise ValueError("Expected message not in captured log records")
+
+ def test_log_task_received_custom(self, caplog):
+ caplog.set_level(logging.INFO, logger="celery.worker.strategy")
+ custom_fmt = "CUSTOM MESSAGE"
+ with self._context(
+ self.add.s(2, 2)
+ ) as C, patch(
+ "celery.app.trace.LOG_RECEIVED", new=custom_fmt,
+ ):
+ C()
+ for record in caplog.records:
+ if record.msg == custom_fmt:
+ assert set(record.args) == {"id", "name"}
+ break
+ else:
+ raise ValueError("Expected message not in captured log records")
+
def test_signal_task_received(self):
callback = Mock()
with self._context(self.add.s(2, 2)) as C:
From e737fbb82b7eec41aa42491e8a331bcc45f9df81 Mon Sep 17 00:00:00 2001
From: Josue Balandrano Coronel
Date: Wed, 19 May 2021 07:06:56 -0500
Subject: [PATCH 228/415] Add What's new for v5.1.0 (#6762)
* Add What's new for v5.1.0
* Update docs
* Update index.
* Fix title formatting.
* Update the title in the migration guide.
* Fix typo.
* Update codename.
* Format code example correctly.
* Update codename in readme file.
* Describe azure 7.0.0 changes
* Fix formatting.
* Update changelog.
* Readd the whats new docs for 5.0.
Co-authored-by: Omer Katz
---
Changelog.rst | 12 +
README.rst | 2 +-
celery/__init__.py | 2 +-
docs/history/index.rst | 2 +
docs/{ => history}/whatsnew-5.0.rst | 0
docs/index.rst | 2 +-
docs/whatsnew-5.1.rst | 435 ++++++++++++++++++++++++++++
7 files changed, 452 insertions(+), 3 deletions(-)
rename docs/{ => history}/whatsnew-5.0.rst (100%)
create mode 100644 docs/whatsnew-5.1.rst
diff --git a/Changelog.rst b/Changelog.rst
index 026ed077fb2..f996674d368 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -8,6 +8,18 @@ This document contains change notes for bugfix & new features
in the & 5.1.x series, please see :ref:`whatsnew-5.1` for
an overview of what's new in Celery 5.1.
+.. _version-5.1.0rc1:
+
+5.1.0rc1
+========
+:release-date: 2021-05-02 16.06 P.M UTC+3:00
+:release-by: Omer Katz
+
+- Celery Mailbox accept and serializer parameters are initialized from configuration. (#6757)
+- Error propagation and errback calling for group-like signatures now works as expected. (#6746)
+- Fix sanitization of passwords in sentinel URIs. (#6765)
+- Add LOG_RECEIVED to customize logging. (#6758)
+
.. _version-5.1.0b2:
5.1.0b2
diff --git a/README.rst b/README.rst
index 8e01787c437..a05bfd033de 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
|build-status| |coverage| |license| |wheel| |pyversion| |pyimp| |ocbackerbadge| |ocsponsorbadge|
-:Version: 5.1.0b2 (singularity)
+:Version: 5.1.0b2 (sun-harmonics)
:Web: https://docs.celeryproject.org/en/stable/index.html
:Download: https://pypi.org/project/celery/
:Source: https://github.com/celery/celery/
diff --git a/celery/__init__.py b/celery/__init__.py
index a5f7f2f49a5..8d84ec8fcb9 100644
--- a/celery/__init__.py
+++ b/celery/__init__.py
@@ -15,7 +15,7 @@
# Lazy loading
from . import local # noqa
-SERIES = 'singularity'
+SERIES = 'sun-harmonics'
__version__ = '5.1.0b2'
__author__ = 'Ask Solem'
diff --git a/docs/history/index.rst b/docs/history/index.rst
index 05dd08a17dc..88e30c0a2b0 100644
--- a/docs/history/index.rst
+++ b/docs/history/index.rst
@@ -13,6 +13,8 @@ version please visit :ref:`changelog`.
.. toctree::
:maxdepth: 2
+ whatsnew-5.0
+ changelog-5.0
whatsnew-4.4
changelog-4.4
whatsnew-4.3
diff --git a/docs/whatsnew-5.0.rst b/docs/history/whatsnew-5.0.rst
similarity index 100%
rename from docs/whatsnew-5.0.rst
rename to docs/history/whatsnew-5.0.rst
diff --git a/docs/index.rst b/docs/index.rst
index 2a9de61c06d..6b93a9d23fc 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -58,7 +58,7 @@ Contents
tutorials/index
faq
changelog
- whatsnew-5.0
+ whatsnew-5.1
reference/index
internals/index
history/index
diff --git a/docs/whatsnew-5.1.rst b/docs/whatsnew-5.1.rst
new file mode 100644
index 00000000000..f6ebae94f08
--- /dev/null
+++ b/docs/whatsnew-5.1.rst
@@ -0,0 +1,435 @@
+.. _whatsnew-5.1:
+
+=========================================
+ What's new in Celery 5.1 (Sun Harmonics)
+=========================================
+:Author: Josue Balandrano Coronel (``jbc at rmcomplexity.com``)
+
+.. sidebar:: Change history
+
+ What's new documents describe the changes in major versions,
+ we also have a :ref:`changelog` that lists the changes in bugfix
+ releases (0.0.x), while older series are archived under the :ref:`history`
+ section.
+
+Celery is a simple, flexible, and reliable distributed programming framework
+to process vast amounts of messages, while providing operations with
+the tools required to maintain a distributed system with python.
+
+It's a task queue with focus on real-time processing, while also
+supporting task scheduling.
+
+Celery has a large and diverse community of users and contributors,
+you should come join us :ref:`on IRC `
+or :ref:`our mailing-list `.
+
+To read more about Celery you should go read the :ref:`introduction `.
+
+While this version is **mostly** backward compatible with previous versions
+it's important that you read the following section as this release
+is a new major version.
+
+This version is officially supported on CPython 3.6, 3.7 & 3.8 & 3.9
+and is also supported on PyPy3.
+
+.. _`website`: http://celeryproject.org/
+
+.. topic:: Table of Contents
+
+ Make sure you read the important notes before upgrading to this version.
+
+.. contents::
+ :local:
+ :depth: 2
+
+Preface
+=======
+
+The 5.1.0 release is a new minor release for Celery.
+
+Starting from now users should expect more frequent releases of major versions
+as we move fast and break things to bring you even better experience.
+
+Releases in the 5.x series are codenamed after songs of `Jon Hopkins `_.
+This release has been codenamed `Sun Harmonics `_.
+
+From now on we only support Python 3.6 and above.
+We will maintain compatibility with Python 3.6 until it's
+EOL in December, 2021.
+
+*— Omer Katz*
+
+Long Term Support Policy
+------------------------
+
+As we'd like to provide some time for you to transition,
+we're designating Celery 4.x an LTS release.
+Celery 4.x will be supported until the 1st of August, 2021.
+
+We will accept and apply patches for bug fixes and security issues.
+However, no new features will be merged for that version.
+
+Celery 5.x **is not** an LTS release. We will support it until the release
+of Celery 6.x.
+
+We're in the process of defining our Long Term Support policy.
+Watch the next "What's New" document for updates.
+
+Wall of Contributors
+--------------------
+
+0xflotus <0xflotus@gmail.com>
+AbdealiJK
+Akash Agrawal
+Anatoliy
+Anna Borzenko
+Anthony Lukach
+Arnon Yaari
+Artem Bernatskyi
+aruseni
+Asif Saif Uddin (Auvi)
+Asif Saif Uddin
+Awais Qureshi
+bastb
+Bas ten Berge
+careljonkhout
+Christian Clauss
+danthegoodman1
+David Pärsson
+David Schneider
+Egor Sergeevich Poderiagin
+elonzh
+Fahmi
+Felix Yan
+František Zatloukal
+Frazer McLean
+Gabriel Augendre
+galcohen
+gal cohen
+Geunsik Lim
+Guillaume DE SUSANNE D'EPINAY
+Hilmar Hilmarsson
+Illia Volochii
+jenhaoyang