djanquiltdb-plugin-celery 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- djanquiltdb_plugin_celery/__init__.py +52 -0
- djanquiltdb_plugin_celery/checks.py +79 -0
- djanquiltdb_plugin_celery/decorators.py +289 -0
- djanquiltdb_plugin_celery/settings.py +75 -0
- djanquiltdb_plugin_celery/targets.py +241 -0
- djanquiltdb_plugin_celery/tasks.py +416 -0
- djanquiltdb_plugin_celery/utils.py +178 -0
- djanquiltdb_plugin_celery-1.0.0.dist-info/METADATA +137 -0
- djanquiltdb_plugin_celery-1.0.0.dist-info/RECORD +13 -0
- djanquiltdb_plugin_celery-1.0.0.dist-info/WHEEL +5 -0
- djanquiltdb_plugin_celery-1.0.0.dist-info/entry_points.txt +2 -0
- djanquiltdb_plugin_celery-1.0.0.dist-info/licenses/LICENSE +23 -0
- djanquiltdb_plugin_celery-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This package defines the plugin object, which DjanQuiltDB finds through the ``djanquiltdb.plugins`` entry-point group,
|
|
3
|
+
and exports the public API of the plugin. DjanQuiltDB serves the decorators as ``djanquiltdb.decorators.celery``, and
|
|
4
|
+
the helpers as ``djanquiltdb.utils.celery``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
from djanquiltdb_plugin_celery import decorators, utils
|
|
10
|
+
from djanquiltdb_plugin_celery.decorators import (
|
|
11
|
+
task_for_active_shard,
|
|
12
|
+
task_for_every_node,
|
|
13
|
+
task_for_every_shard,
|
|
14
|
+
task_for_node,
|
|
15
|
+
task_for_shard,
|
|
16
|
+
)
|
|
17
|
+
from djanquiltdb_plugin_celery.utils import (
|
|
18
|
+
aggregate_per_shard,
|
|
19
|
+
fan_out,
|
|
20
|
+
fan_out_to_nodes,
|
|
21
|
+
fanout_shards,
|
|
22
|
+
per_node_signatures,
|
|
23
|
+
per_shard_signatures,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__version__ = '1.0.0'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def install():
|
|
30
|
+
"""
|
|
31
|
+
Registers the plugin's system checks. DjanQuiltDB calls install() once the app registry is ready.
|
|
32
|
+
"""
|
|
33
|
+
from djanquiltdb_plugin_celery import checks # noqa: F401
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
plugin = SimpleNamespace(decorators=decorators, utils=utils, install=install)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
'aggregate_per_shard',
|
|
40
|
+
'fan_out',
|
|
41
|
+
'fan_out_to_nodes',
|
|
42
|
+
'fanout_shards',
|
|
43
|
+
'install',
|
|
44
|
+
'per_node_signatures',
|
|
45
|
+
'per_shard_signatures',
|
|
46
|
+
'plugin',
|
|
47
|
+
'task_for_active_shard',
|
|
48
|
+
'task_for_every_node',
|
|
49
|
+
'task_for_every_shard',
|
|
50
|
+
'task_for_node',
|
|
51
|
+
'task_for_shard',
|
|
52
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.core import checks
|
|
3
|
+
from djanquiltdb.utils import get_mapping_class
|
|
4
|
+
|
|
5
|
+
from djanquiltdb_plugin_celery.settings import (
|
|
6
|
+
QUEUE_DESCRIPTION,
|
|
7
|
+
QUILT_CELERY_DEFAULTS,
|
|
8
|
+
SPREAD_DESCRIPTION,
|
|
9
|
+
is_spread,
|
|
10
|
+
)
|
|
11
|
+
from djanquiltdb_plugin_celery.targets import NODE_NAME_KWARG, SHARD_ID_KWARG
|
|
12
|
+
|
|
13
|
+
#: For each key of QUILT_CELERY: a function that returns whether a value of the key is valid, and how a message
|
|
14
|
+
#: describes the valid values. A boolean is an instance of int, so the keys that accept an integer and not a boolean
|
|
15
|
+
#: exclude it.
|
|
16
|
+
QUILT_CELERY_VALIDATORS = {
|
|
17
|
+
'MAPPING_KWARG': (lambda value: isinstance(value, (str, type(None))), 'a string or None'),
|
|
18
|
+
'MAX_RETRIES': (
|
|
19
|
+
lambda value: value is None or (isinstance(value, int) and not isinstance(value, bool)),
|
|
20
|
+
'an integer or None',
|
|
21
|
+
),
|
|
22
|
+
'RETRY_BACKOFF': (lambda value: isinstance(value, (bool, int)), 'a boolean or an integer'),
|
|
23
|
+
'RETRY_BACKOFF_MAX': (lambda value: isinstance(value, int) and not isinstance(value, bool), 'an integer'),
|
|
24
|
+
'RETRY_JITTER': (lambda value: isinstance(value, bool), 'a boolean'),
|
|
25
|
+
'FANOUT_QUEUE': (lambda value: isinstance(value, (str, type(None))), QUEUE_DESCRIPTION),
|
|
26
|
+
'FANOUT_SPREAD': (is_spread, SPREAD_DESCRIPTION),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@checks.register(checks.Tags.compatibility)
|
|
31
|
+
def check_quilt_celery_setting(app_configs, **kwargs):
|
|
32
|
+
"""
|
|
33
|
+
Reports a QUILT_CELERY setting, key or value that the celery plugin cannot use.
|
|
34
|
+
"""
|
|
35
|
+
quilt_celery = getattr(settings, 'QUILT_CELERY', {})
|
|
36
|
+
|
|
37
|
+
if not isinstance(quilt_celery, dict):
|
|
38
|
+
return [checks.Error('QUILT_CELERY is not a dict.', id='djanquiltdb_plugin_celery.E002')]
|
|
39
|
+
|
|
40
|
+
messages = [
|
|
41
|
+
checks.Error(
|
|
42
|
+
'QUILT_CELERY has the key {!r}, which the celery plugin does not read.'.format(name),
|
|
43
|
+
hint='The plugin reads the keys {}.'.format(', '.join(QUILT_CELERY_DEFAULTS)),
|
|
44
|
+
id='djanquiltdb_plugin_celery.E001',
|
|
45
|
+
)
|
|
46
|
+
for name in quilt_celery
|
|
47
|
+
if name not in QUILT_CELERY_DEFAULTS
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
for name, (is_valid, description) in QUILT_CELERY_VALIDATORS.items():
|
|
51
|
+
if name in quilt_celery and not is_valid(quilt_celery[name]):
|
|
52
|
+
messages.append(
|
|
53
|
+
checks.Error(
|
|
54
|
+
"QUILT_CELERY['{}'] is {!r}, and it accepts {}.".format(name, quilt_celery[name], description),
|
|
55
|
+
id='djanquiltdb_plugin_celery.E002',
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if quilt_celery.get('MAPPING_KWARG') in (SHARD_ID_KWARG, NODE_NAME_KWARG):
|
|
60
|
+
messages.append(
|
|
61
|
+
checks.Error(
|
|
62
|
+
"QUILT_CELERY['MAPPING_KWARG'] is {!r}, which already identifies a target in another way.".format(
|
|
63
|
+
quilt_celery['MAPPING_KWARG']
|
|
64
|
+
),
|
|
65
|
+
hint='Set it to the keyword argument that has the mapping value, such as tenant_id.',
|
|
66
|
+
id='djanquiltdb_plugin_celery.E002',
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
elif quilt_celery.get('MAPPING_KWARG') and get_mapping_class() is None:
|
|
70
|
+
messages.append(
|
|
71
|
+
checks.Error(
|
|
72
|
+
"QUILT_CELERY['MAPPING_KWARG'] is {!r}, but QUILT_DB has no MAPPING_MODEL in which a task can look up "
|
|
73
|
+
'the shard of a mapping value.'.format(quilt_celery['MAPPING_KWARG']),
|
|
74
|
+
hint="Set QUILT_DB['MAPPING_MODEL'], or remove QUILT_CELERY['MAPPING_KWARG'].",
|
|
75
|
+
id='djanquiltdb_plugin_celery.E002',
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return messages
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The task decorators of the plugin. DjanQuiltDB serves this module as ``djanquiltdb.decorators.celery``. Because
|
|
3
|
+
``SERVE_UNQUALIFIED`` is False, DjanQuiltDB does not also serve these decorators as attributes of
|
|
4
|
+
``djanquiltdb.decorators``, so that a call site names the plugin, as in ``@celery.task_for_shard``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import annotationlib
|
|
8
|
+
import functools
|
|
9
|
+
import inspect
|
|
10
|
+
|
|
11
|
+
from celery import shared_task
|
|
12
|
+
from djanquiltdb.utils import StateException
|
|
13
|
+
|
|
14
|
+
from djanquiltdb_plugin_celery.settings import ensure_valid_queue, ensure_valid_spread, quilt_celery_setting
|
|
15
|
+
from djanquiltdb_plugin_celery.targets import use_target
|
|
16
|
+
from djanquiltdb_plugin_celery.tasks import QuiltTask
|
|
17
|
+
|
|
18
|
+
SERVE_UNQUALIFIED = False
|
|
19
|
+
|
|
20
|
+
#: The signature of the wrapper of each function: the task, and any arguments.
|
|
21
|
+
WRAPPER_SIGNATURE = inspect.Signature(
|
|
22
|
+
[
|
|
23
|
+
inspect.Parameter('self', inspect.Parameter.POSITIONAL_ONLY),
|
|
24
|
+
inspect.Parameter('args', inspect.Parameter.VAR_POSITIONAL),
|
|
25
|
+
inspect.Parameter('kwargs', inspect.Parameter.VAR_KEYWORD),
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _retry_options(options):
|
|
31
|
+
"""
|
|
32
|
+
Returns the Celery options that retry a task while its shard is in maintenance, with the values of ``options``
|
|
33
|
+
taking precedence over the QUILT_CELERY settings.
|
|
34
|
+
"""
|
|
35
|
+
return {
|
|
36
|
+
'max_retries': quilt_celery_setting('MAX_RETRIES'),
|
|
37
|
+
'retry_backoff': quilt_celery_setting('RETRY_BACKOFF'),
|
|
38
|
+
'retry_backoff_max': quilt_celery_setting('RETRY_BACKOFF_MAX'),
|
|
39
|
+
'retry_jitter': quilt_celery_setting('RETRY_JITTER'),
|
|
40
|
+
**options,
|
|
41
|
+
'autoretry_for': (StateException, *options.get('autoretry_for', ())),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _signature_of_function(func, bind):
|
|
46
|
+
"""
|
|
47
|
+
Returns the signature of ``func`` without the parameter for the task, which ``func`` takes when ``bind`` is True.
|
|
48
|
+
The arguments of each message of the task are checked against this signature.
|
|
49
|
+
"""
|
|
50
|
+
signature = inspect.signature(func, annotation_format=annotationlib.Format.STRING)
|
|
51
|
+
parameters = list(signature.parameters.values())
|
|
52
|
+
|
|
53
|
+
return signature.replace(parameters=parameters[1:] if bind else parameters)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _create_quilt_task(func, task_attributes, options):
|
|
57
|
+
"""
|
|
58
|
+
Returns the task of ``func``, with ``task_attributes`` as attributes of its QuiltTask class and ``options`` as
|
|
59
|
+
the options of ``celery.shared_task()``.
|
|
60
|
+
"""
|
|
61
|
+
ensure_valid_spread(task_attributes.get('fanout_spread'), func.__qualname__)
|
|
62
|
+
ensure_valid_queue(task_attributes.get('fanout_queue'), func.__qualname__)
|
|
63
|
+
|
|
64
|
+
options = dict(options)
|
|
65
|
+
bind = options.pop('bind', False)
|
|
66
|
+
base = options.pop('base', QuiltTask)
|
|
67
|
+
|
|
68
|
+
if not issubclass(base, QuiltTask):
|
|
69
|
+
raise TypeError(
|
|
70
|
+
'The base of {} is {}, which does not inherit from QuiltTask. The tasks of the celery plugin need '
|
|
71
|
+
'the methods of QuiltTask to send and to enter their targets.'.format(func.__qualname__, base.__qualname__)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# The wrapper is always bound, because it reads the request of the task. The function receives the task only when it
|
|
75
|
+
# is decorated with bind=True.
|
|
76
|
+
@functools.wraps(func)
|
|
77
|
+
def run_in_target(self, *args, **kwargs):
|
|
78
|
+
target = self.target_of_message(self.request.headers, kwargs)
|
|
79
|
+
|
|
80
|
+
if target is None and self.fans_out:
|
|
81
|
+
return self.fan_out_to_every_target(args, kwargs)
|
|
82
|
+
|
|
83
|
+
if target is None:
|
|
84
|
+
raise self.missing_target_error(kwargs)
|
|
85
|
+
|
|
86
|
+
with use_target(target, lock=self.lock_target):
|
|
87
|
+
function_kwargs = self.kwargs_for_function(kwargs, self.entered_target_value(target))
|
|
88
|
+
|
|
89
|
+
return func(self, *args, **function_kwargs) if bind else func(*args, **function_kwargs)
|
|
90
|
+
|
|
91
|
+
# functools.wraps makes the wrapper report the signature of the function, and Celery checks the arguments of a
|
|
92
|
+
# message against the signature of the wrapper. The function does not receive a keyword argument of the target that
|
|
93
|
+
# it does not declare, so the wrapper takes any arguments, and QuiltTask checks the arguments in the form in which
|
|
94
|
+
# the function receives them.
|
|
95
|
+
run_in_target.__signature__ = WRAPPER_SIGNATURE
|
|
96
|
+
task_attributes = {**task_attributes, 'function_signature': _signature_of_function(func, bind)}
|
|
97
|
+
|
|
98
|
+
return shared_task(bind=True, base=base, **task_attributes, **_retry_options(options))(run_in_target)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _task_decorator_or_task(task_attributes, func, options):
|
|
102
|
+
"""
|
|
103
|
+
Returns the task of ``func``, or, when ``func`` is None, the decorator that returns the task, so that each decorator
|
|
104
|
+
works with and without arguments.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def decorate(func):
|
|
108
|
+
return _create_quilt_task(func, task_attributes, options)
|
|
109
|
+
|
|
110
|
+
return decorate if func is None else decorate(func)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def task_for_shard(func=None, *, inherit_active_shard=False, lock=None, **options):
|
|
114
|
+
"""
|
|
115
|
+
Turns a function into a Celery task that runs in a shard: the shard that ``for_shard()`` is given, else the shard
|
|
116
|
+
that its keyword arguments identify. That shard is the target of the task. ``shard_id`` identifies a shard by its
|
|
117
|
+
primary key. The mapping field of the ``MAPPING_MODEL``, such as ``tenant_id``, identifies a shard by its
|
|
118
|
+
mapping value, and ``QUILT_CELERY['MAPPING_KWARG']`` can set another keyword argument for the mapping value.
|
|
119
|
+
|
|
120
|
+
The function receives ``shard_id`` and the keyword argument for a mapping value only when it declares them by name,
|
|
121
|
+
and ``**kwargs`` does not receive them. A function that declares ``shard_id`` receives the primary key of the shard
|
|
122
|
+
that the task runs in, also when the task is sent without ``shard_id``. ``node_name`` is an ordinary keyword
|
|
123
|
+
argument for the function: a task that runs on a node is decorated with :func:`task_for_node`. The arguments of the
|
|
124
|
+
task are checked against the function when the task is sent, unless the task is decorated with ``typing=False``.
|
|
125
|
+
|
|
126
|
+
Sending the task without a shard raises TaskShardError, unless the task inherits the active shard and a shard is
|
|
127
|
+
active where the task is sent. Sending the task with two shards raises TaskShardError too. While the shard is in
|
|
128
|
+
maintenance, the task is retried with an exponential backoff, and after the last retry the task fails with the
|
|
129
|
+
StateException.
|
|
130
|
+
|
|
131
|
+
:param inherit_active_shard: When True, a task that is sent without a shard in its keyword arguments runs in the
|
|
132
|
+
shard that is active where it is sent, as :func:`task_for_active_shard` does.
|
|
133
|
+
:param lock: Passed on to ``use_shard()`` as ``lock``. By default, ``use_shard()`` locks the shard while the task
|
|
134
|
+
runs.
|
|
135
|
+
:param options: The options of ``celery.shared_task()``. ``max_retries``, ``retry_backoff``, ``retry_backoff_max``
|
|
136
|
+
and ``retry_jitter`` default to the QUILT_CELERY settings, and StateException is added to the exceptions of
|
|
137
|
+
``autoretry_for``. ``base`` defaults to :class:`~djanquiltdb_plugin_celery.tasks.QuiltTask`, and a
|
|
138
|
+
``base`` that does not inherit from that class raises TypeError.
|
|
139
|
+
|
|
140
|
+
:Example:
|
|
141
|
+
.. code-block:: python
|
|
142
|
+
|
|
143
|
+
from djanquiltdb.decorators.celery import task_for_shard
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@task_for_shard
|
|
147
|
+
def send_invoice(invoice_id):
|
|
148
|
+
Invoice.objects.get(pk=invoice_id).send()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
send_invoice.for_shard(shard).delay(invoice_id=12)
|
|
152
|
+
send_invoice.delay_for_shard(shard, invoice_id=12) # the same
|
|
153
|
+
send_invoice.delay(invoice_id=12, tenant_id=3)
|
|
154
|
+
"""
|
|
155
|
+
task_attributes = {'runs_in': 'shard', 'lock_target': lock, 'inherit_active_shard': inherit_active_shard}
|
|
156
|
+
|
|
157
|
+
return _task_decorator_or_task(task_attributes, func, options)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def task_for_node(func=None, **options):
|
|
161
|
+
"""
|
|
162
|
+
Turns a function into a Celery task that runs in the public schema of a node: the node that ``for_node()`` is given,
|
|
163
|
+
else the node that its ``node_name`` keyword argument identifies. That schema is the target of the task. The
|
|
164
|
+
function receives ``node_name`` only when it declares it by name, and then also when the task is sent without
|
|
165
|
+
``node_name``. ``shard_id`` and the keyword argument for a mapping value are ordinary keyword arguments for the
|
|
166
|
+
function. Sending the task without a node raises TaskNodeError. The public schema of a node has no lock, so the task
|
|
167
|
+
takes no ``lock``. ``options`` work as they do for :func:`task_for_shard`.
|
|
168
|
+
|
|
169
|
+
:Example:
|
|
170
|
+
.. code-block:: python
|
|
171
|
+
|
|
172
|
+
from djanquiltdb.decorators.celery import task_for_node
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@task_for_node
|
|
176
|
+
def refresh_exchange_rates():
|
|
177
|
+
ExchangeRate.objects.refresh()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
refresh_exchange_rates.for_node('other').delay()
|
|
181
|
+
refresh_exchange_rates.delay(node_name='other') # the same
|
|
182
|
+
"""
|
|
183
|
+
return _task_decorator_or_task({'runs_in': 'node'}, func, options)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def task_for_active_shard(func=None, **options):
|
|
187
|
+
"""
|
|
188
|
+
:func:`task_for_shard` for a task that runs in the shard that is active where the task is sent: inside
|
|
189
|
+
``use_shard()``, ``use_shard_for()`` or a request that the DjanQuiltDB middleware wraps. The shard is resolved when
|
|
190
|
+
the task is sent, and is sent in the message header. The shard of ``for_shard()`` and a shard in the keyword
|
|
191
|
+
arguments take precedence over the active shard. Sending the task without a shard while no shard is active, or while
|
|
192
|
+
only the public schema of a node is active, raises TaskShardError.
|
|
193
|
+
|
|
194
|
+
:Example:
|
|
195
|
+
.. code-block:: python
|
|
196
|
+
|
|
197
|
+
from djanquiltdb.decorators.celery import task_for_active_shard
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@task_for_active_shard
|
|
201
|
+
def recount_invoices():
|
|
202
|
+
Invoice.objects.recount()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
with use_shard(shard):
|
|
206
|
+
recount_invoices.delay() # runs in shard
|
|
207
|
+
recount_invoices.delay_for_shard(other_shard) # runs in other_shard
|
|
208
|
+
"""
|
|
209
|
+
return task_for_shard(func, inherit_active_shard=True, **options)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def task_for_every_shard(func=None, *, queue=None, spread=None, include_maintenance=False, lock=None, **options):
|
|
213
|
+
"""
|
|
214
|
+
:func:`task_for_shard` for a task that runs once in every active shard when it is sent without a shard. The message
|
|
215
|
+
that is sent without a shard starts a fan-out where it runs: the task sends itself once for each active shard, with
|
|
216
|
+
the ``shard_id`` of the shard added to the keyword arguments. So a periodic task in the beat schedule needs only the
|
|
217
|
+
name of the task. A task that is sent with a shard, in ``for_shard()`` or in its keyword arguments, runs in that
|
|
218
|
+
shard only. The task does not inherit the active shard, so that it also runs in every active shard when it is sent
|
|
219
|
+
inside ``use_shard()``.
|
|
220
|
+
|
|
221
|
+
The function receives ``shard_id`` only when it declares it by name, as for :func:`task_for_shard`. A function that
|
|
222
|
+
declares ``shard_id`` can declare it without a default, because the check of the arguments of a message counts
|
|
223
|
+
``shard_id`` as given. The parameters after ``shard_id`` are passed by keyword, as ``shard_id`` is.
|
|
224
|
+
|
|
225
|
+
:param queue: The name of the queue of each task that the fan-out sends. Defaults to
|
|
226
|
+
``QUILT_CELERY['FANOUT_QUEUE']``, else the routing of the Celery app, whose ``task_routes`` can route each task
|
|
227
|
+
by its ``shard_id``. A queue that is not a string raises ValueError.
|
|
228
|
+
:param spread: Delays each task that the fan-out sends by a random number of seconds: from 0 to an integer, or
|
|
229
|
+
between a pair of integers, which are not negative and in ascending order. Defaults to
|
|
230
|
+
``QUILT_CELERY['FANOUT_SPREAD']``. Any other value raises ValueError.
|
|
231
|
+
:param include_maintenance: When True, the fan-out also sends the task to the shards in maintenance, where it is
|
|
232
|
+
retried as in any shard in maintenance, and fails with the StateException after the last retry.
|
|
233
|
+
|
|
234
|
+
:Example:
|
|
235
|
+
.. code-block:: python
|
|
236
|
+
|
|
237
|
+
from djanquiltdb.decorators.celery import task_for_every_shard
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@task_for_every_shard(queue='fanout', spread=(0, 300))
|
|
241
|
+
def clean_up_drafts():
|
|
242
|
+
Draft.objects.filter(updated_at__lt=a_month_ago()).delete()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
clean_up_drafts.delay() # runs once in every active shard
|
|
246
|
+
"""
|
|
247
|
+
task_attributes = {
|
|
248
|
+
'runs_in': 'shard',
|
|
249
|
+
'lock_target': lock,
|
|
250
|
+
'fans_out': True,
|
|
251
|
+
'fanout_queue': queue,
|
|
252
|
+
'fanout_spread': spread,
|
|
253
|
+
'fanout_include_maintenance': include_maintenance,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return _task_decorator_or_task(task_attributes, func, options)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def task_for_every_node(func=None, *, queue=None, spread=None, **options):
|
|
260
|
+
"""
|
|
261
|
+
:func:`task_for_node` for a task that runs once in the public schema of every node when it is sent without a
|
|
262
|
+
``node_name``. The message that is sent without a ``node_name`` starts a fan-out where it runs: the task sends
|
|
263
|
+
itself once for each node in ``DATABASES``, with the ``node_name`` of the node added to the keyword arguments. A
|
|
264
|
+
task that is sent with a node, in ``for_node()`` or in its keyword arguments, runs on that node only. The function
|
|
265
|
+
receives ``node_name`` only when it declares it by name. A function that declares ``node_name`` can declare it
|
|
266
|
+
without a default, and the parameters after ``node_name`` are passed by keyword. ``queue`` and ``spread`` work as
|
|
267
|
+
they do for :func:`task_for_every_shard`.
|
|
268
|
+
|
|
269
|
+
:Example:
|
|
270
|
+
.. code-block:: python
|
|
271
|
+
|
|
272
|
+
from djanquiltdb.decorators.celery import task_for_every_node
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@task_for_every_node
|
|
276
|
+
def refresh_exchange_rates():
|
|
277
|
+
ExchangeRate.objects.refresh()
|
|
278
|
+
"""
|
|
279
|
+
task_attributes = {
|
|
280
|
+
'runs_in': 'node',
|
|
281
|
+
'fans_out': True,
|
|
282
|
+
'fanout_queue': queue,
|
|
283
|
+
'fanout_spread': spread,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return _task_decorator_or_task(task_attributes, func, options)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
__all__ = ['task_for_active_shard', 'task_for_every_node', 'task_for_every_shard', 'task_for_node', 'task_for_shard']
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The ``QUILT_CELERY`` setting, an optional dict whose keys override the defaults below.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
|
|
7
|
+
QUILT_CELERY_DEFAULTS = {
|
|
8
|
+
# The keyword argument that identifies a shard by its mapping value, which is not shard_id or node_name. None means
|
|
9
|
+
# the mapping field of the MAPPING_MODEL.
|
|
10
|
+
'MAPPING_KWARG': None,
|
|
11
|
+
# Celery's max_retries, retry_backoff, retry_backoff_max and retry_jitter options for a task whose shard is in
|
|
12
|
+
# maintenance. A decorator argument overrides each of them. The defaults retry after 5, 10, 20, 40 and 60 seconds.
|
|
13
|
+
'MAX_RETRIES': 5,
|
|
14
|
+
'RETRY_BACKOFF': 5,
|
|
15
|
+
'RETRY_BACKOFF_MAX': 60,
|
|
16
|
+
'RETRY_JITTER': False,
|
|
17
|
+
# The queue of each task that a fan-out sends. None means the routing of the Celery app.
|
|
18
|
+
'FANOUT_QUEUE': None,
|
|
19
|
+
# The countdown of each task that a fan-out sends: a random number of seconds from 0 to an integer, or between a
|
|
20
|
+
# pair of integers, which are not negative and in ascending order. None means no countdown.
|
|
21
|
+
'FANOUT_SPREAD': None,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#: How a message describes the values that a spread accepts.
|
|
25
|
+
SPREAD_DESCRIPTION = 'an integer that is not negative, a pair of such integers in ascending order, or None'
|
|
26
|
+
|
|
27
|
+
#: How a message describes the values that a queue accepts.
|
|
28
|
+
QUEUE_DESCRIPTION = 'a string or None'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def quilt_celery_setting(name):
|
|
32
|
+
"""
|
|
33
|
+
Returns the value of ``name`` in QUILT_CELERY, else the default of ``name``. When QUILT_CELERY is not a dict, the
|
|
34
|
+
function returns the default, so that a tasks module can be imported and the system check can report the setting.
|
|
35
|
+
"""
|
|
36
|
+
quilt_celery = getattr(settings, 'QUILT_CELERY', {})
|
|
37
|
+
|
|
38
|
+
if not isinstance(quilt_celery, dict):
|
|
39
|
+
quilt_celery = {}
|
|
40
|
+
|
|
41
|
+
return quilt_celery.get(name, QUILT_CELERY_DEFAULTS[name])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_spread(value):
|
|
45
|
+
"""
|
|
46
|
+
Returns whether ``value`` is a spread of the countdown of a fan-out: None, an integer that is not negative, or a
|
|
47
|
+
pair of such integers in ascending order. A boolean is an instance of int, and is not a spread.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def is_bound(bound):
|
|
51
|
+
return isinstance(bound, int) and not isinstance(bound, bool) and bound >= 0
|
|
52
|
+
|
|
53
|
+
if value is None:
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
if isinstance(value, int):
|
|
57
|
+
return is_bound(value)
|
|
58
|
+
|
|
59
|
+
return isinstance(value, (list, tuple)) and len(value) == 2 and all(map(is_bound, value)) and value[0] <= value[1]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def ensure_valid_spread(spread, owner):
|
|
63
|
+
"""
|
|
64
|
+
Raises ValueError, whose message includes ``owner``, when ``spread`` is not a spread of the countdown of a fan-out.
|
|
65
|
+
"""
|
|
66
|
+
if not is_spread(spread):
|
|
67
|
+
raise ValueError('The spread of {} is {!r}, and it accepts {}.'.format(owner, spread, SPREAD_DESCRIPTION))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def ensure_valid_queue(queue, owner):
|
|
71
|
+
"""
|
|
72
|
+
Raises ValueError, whose message includes ``owner``, when ``queue`` is neither a string nor None.
|
|
73
|
+
"""
|
|
74
|
+
if not isinstance(queue, (str, type(None))):
|
|
75
|
+
raise ValueError('The queue of {} is {!r}, and it accepts {}.'.format(owner, queue, QUEUE_DESCRIPTION))
|