p1-taskqueue 0.1.12__py3-none-any.whl → 0.1.13__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.

Potentially problematic release.


This version of p1-taskqueue might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: p1-taskqueue
3
- Version: 0.1.12
3
+ Version: 0.1.13
4
4
  Summary: A Task Queue Wrapper for Dekoruma Backend
5
5
  Author-email: Chalvin <engineering@dekoruma.com>
6
6
  Project-URL: Homepage, https://github.com/Dekoruma/p1-taskqueue
@@ -19,6 +19,7 @@ Requires-Dist: kombu>=5.5.4
19
19
  Requires-Dist: django>=4.0.0
20
20
  Requires-Dist: django-celery-results>=2.6.0
21
21
  Requires-Dist: django-celery-beat>=2.8.1
22
+ Requires-Dist: requests>=2.32.3
22
23
  Provides-Extra: dev
23
24
  Requires-Dist: pytest>=7.0.0; extra == "dev"
24
25
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
@@ -0,0 +1,10 @@
1
+ taskqueue/__init__.py,sha256=nNHXIyysNLom9f8of-d__pWJ-YF53Mbrbsb1frPzPPI,298
2
+ taskqueue/celery_app.py,sha256=rLIYRBBgYaQiXEqz-zCntvS8xU4eFZ9YuhJBfIye9E0,3931
3
+ taskqueue/cmanager.py,sha256=9jxcTsWOpzexV3SkRhlY-PkhrobEdMuQVm6tWVVzgTs,16260
4
+ taskqueue/slack_notifier.py,sha256=ZvTbWa1XXHUiciYF14_SH5af0BvGoPCBQohPtxx4FgU,1419
5
+ taskqueue/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ taskqueue/libs/helper_test.py,sha256=JCdh2S29PpL8RqUxqkcIqwIvr3M9puqHglBZAmfPkuw,7722
7
+ p1_taskqueue-0.1.13.dist-info/METADATA,sha256=pr73Jir2XbTBsq2HieYSEO4h0a_FM5KV15THqfedOM0,1597
8
+ p1_taskqueue-0.1.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ p1_taskqueue-0.1.13.dist-info/top_level.txt,sha256=hA3SM1ik2K8iPqtlt_-_nJ4TAePwHPN3vsOc4EiynqU,10
10
+ p1_taskqueue-0.1.13.dist-info/RECORD,,
taskqueue/celery_app.py CHANGED
@@ -2,10 +2,14 @@
2
2
  Celery application setup for TaskQueue.
3
3
  Reads configuration from Django settings and auto-configures queues with DLQ.
4
4
  """
5
+ import logging
6
+
5
7
  from celery import Celery
6
8
  from kombu import Exchange
7
9
  from kombu import Queue
8
10
 
11
+ logger = logging.getLogger(__name__)
12
+
9
13
 
10
14
  def get_django_settings():
11
15
  """Get Django settings, fail fast if not properly configured."""
@@ -63,7 +67,6 @@ def setup_queues(app, settings, celery_config):
63
67
  queue_names = ['default', 'high', 'low']
64
68
  dlq_name_prefix = getattr(settings, 'TASKQUEUE_DLQ_NAME_PREFIX', 'dlq')
65
69
 
66
- # Create exchanges
67
70
  main_exchange = Exchange(app_name, type='direct')
68
71
  dlx_exchange = Exchange(f'{app_name}.dlx', type='direct')
69
72
 
@@ -95,5 +98,16 @@ def setup_queues(app, settings, celery_config):
95
98
  'task_queues': tuple(queues),
96
99
  })
97
100
 
101
+ try:
102
+ with app.connection_or_acquire() as conn:
103
+ main_exchange.declare(channel=conn.default_channel)
104
+ dlx_exchange.declare(channel=conn.default_channel)
105
+
106
+ for queue in queues:
107
+ queue.declare(channel=conn.default_channel)
108
+ except Exception as e:
109
+ logger.warning(
110
+ f"[TaskQueue] Failed to declare queues: {str(e.__class__.__name__)} {e}")
111
+
98
112
 
99
113
  celery_app = create_celery_app()
taskqueue/cmanager.py CHANGED
@@ -1,13 +1,13 @@
1
1
  import importlib
2
2
  import inspect
3
3
  import logging
4
- from datetime import datetime
5
- from datetime import timedelta
6
4
  from typing import Any
7
5
  from typing import Dict
8
6
  from typing import Tuple
9
7
 
10
8
  from celery import shared_task
9
+ from celery.exceptions import Reject
10
+ from taskqueue.slack_notifier import SlackbotManager
11
11
 
12
12
  # Setup logger
13
13
  logger = logging.getLogger(__name__)
@@ -50,7 +50,8 @@ def _extract_init_args_from_instance(instance: Any) -> Tuple[list, dict]:
50
50
  def _split_function_and_queue_kwargs(kwargs: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
51
51
  # To prevent confusion whether a kwargs is for function or queue kwargs(i.e celery options and on_commit),
52
52
  # ignore confusing kwargs while give warning
53
- supported_queue_keys = {"channel", "retry", "on_commit", "job_timeout"}
53
+ supported_queue_keys = {"channel", "retry",
54
+ "on_commit", "job_timeout", "use_legacy_executor"}
54
55
  ignored_non_function_keys = {
55
56
  "queue", "countdown", "eta", "expires", "priority", "task_id", "routing_key",
56
57
  "serializer", "compression", "headers", "link", "link_error", "retry_policy",
@@ -74,6 +75,17 @@ def _split_function_and_queue_kwargs(kwargs: Dict[str, Any]) -> Tuple[Dict[str,
74
75
  return func_kwargs, queue_kwargs
75
76
 
76
77
 
78
+ def _build_callable_task_call(func: Any, func_args: tuple, func_kwargs: dict) -> Tuple[str, list, dict]:
79
+ task_name = "taskqueue.cmanager.callable_executor"
80
+ task_args = []
81
+ task_kwargs = {
82
+ "callable_obj": func,
83
+ "args": list(func_args),
84
+ "kwargs": dict(func_kwargs),
85
+ }
86
+ return task_name, task_args, task_kwargs
87
+
88
+
77
89
  def _build_dynamic_task_call(func: Any, *args: Any, **func_kwargs: Any) -> Tuple[str, list, dict]:
78
90
  if _is_class_method(func):
79
91
  instance = getattr(func, "__self__")
@@ -155,47 +167,21 @@ class CManager:
155
167
  'enqueue_op_type', K_ENQUEUE_OP_TYPE_ENQUEUE)
156
168
 
157
169
  try:
158
- if enqueue_op_type == K_ENQUEUE_OP_TYPE_ENQUEUE:
159
- if not args:
160
- raise ValueError(
161
- "enqueue requires a callable as the first positional argument")
162
- func = args[0]
163
- func_args = args[1:]
164
-
165
- elif enqueue_op_type == K_ENQUEUE_OP_TYPE_ENQUEUE_AT:
166
- if len(args) < 2:
167
- raise ValueError(
168
- "enqueue_at requires (eta_datetime, func, *func_args)")
169
- eta: datetime = args[0]
170
- func = args[1]
171
- func_args = args[2:]
172
-
173
- elif enqueue_op_type == K_ENQUEUE_OP_TYPE_ENQUEUE_IN:
174
- if len(args) < 2:
175
- raise ValueError(
176
- "enqueue_in requires (countdown_delta, func, *func_args)")
177
- delta: timedelta = args[0]
178
- func = args[1]
179
- func_args = args[2:]
180
- else:
181
- raise ValueError(
182
- f"Unknown enqueue operation type: {enqueue_op_type}")
183
-
184
- func_kwargs, queue_kwargs = _split_function_and_queue_kwargs(
185
- kwargs)
170
+ func, func_args, func_kwargs, queue_options = self._parse_enqueue_args(
171
+ enqueue_op_type, args, kwargs)
186
172
 
187
- if enqueue_op_type == K_ENQUEUE_OP_TYPE_ENQUEUE_AT:
188
- queue_kwargs = dict(queue_kwargs)
189
- queue_kwargs["eta"] = eta
190
- elif enqueue_op_type == K_ENQUEUE_OP_TYPE_ENQUEUE_IN:
191
- queue_kwargs = dict(queue_kwargs)
192
- queue_kwargs["countdown"] = int(delta.total_seconds())
173
+ use_legacy_executor = queue_options.pop(
174
+ 'use_legacy_executor', True)
193
175
 
194
- task_name, task_args, task_kwargs = _build_dynamic_task_call(
195
- func, *func_args, **func_kwargs)
176
+ if use_legacy_executor:
177
+ task_name, task_args, task_kwargs = _build_dynamic_task_call(
178
+ func, *func_args, **func_kwargs)
179
+ else:
180
+ task_name, task_args, task_kwargs = _build_callable_task_call(
181
+ func, func_args, func_kwargs)
196
182
 
197
183
  task_id = self._send_task(task_name, task_args,
198
- task_kwargs, queue_kwargs)
184
+ task_kwargs, queue_options)
199
185
 
200
186
  logger.info('[_enqueue_op_base %s] Submit Celery Task SUCCESS, task_name: %s args: %s, kwargs: %s, task_id: %s' % (
201
187
  enqueue_op_type, task_name, task_args, task_kwargs, task_id))
@@ -205,6 +191,46 @@ class CManager:
205
191
  enqueue_op_type, str(e), args, kwargs))
206
192
  raise e
207
193
 
194
+ def _parse_enqueue_args(self, enqueue_op_type: str, args: tuple, kwargs: dict) -> Tuple[Any, tuple, dict, dict]:
195
+ """Parse enqueue arguments and return func, func_args, func_kwargs, and queue_options."""
196
+ if enqueue_op_type == K_ENQUEUE_OP_TYPE_ENQUEUE:
197
+ if not args:
198
+ raise ValueError(
199
+ "enqueue requires a callable as the first positional argument")
200
+ func = args[0]
201
+ func_args = args[1:]
202
+ eta, delta = None, None
203
+
204
+ elif enqueue_op_type == K_ENQUEUE_OP_TYPE_ENQUEUE_AT:
205
+ if len(args) < 2:
206
+ raise ValueError(
207
+ "enqueue_at requires (eta_datetime, func, *func_args)")
208
+ eta = args[0]
209
+ func = args[1]
210
+ func_args = args[2:]
211
+ delta = None
212
+
213
+ elif enqueue_op_type == K_ENQUEUE_OP_TYPE_ENQUEUE_IN:
214
+ if len(args) < 2:
215
+ raise ValueError(
216
+ "enqueue_in requires (countdown_delta, func, *func_args)")
217
+ delta = args[0]
218
+ func = args[1]
219
+ func_args = args[2:]
220
+ eta = None
221
+ else:
222
+ raise ValueError(
223
+ f"Unknown enqueue operation type: {enqueue_op_type}")
224
+
225
+ func_kwargs, queue_options = _split_function_and_queue_kwargs(kwargs)
226
+
227
+ if eta is not None:
228
+ queue_options["eta"] = eta
229
+ elif delta is not None:
230
+ queue_options["countdown"] = int(delta.total_seconds())
231
+
232
+ return func, func_args, func_kwargs, queue_options
233
+
208
234
  def _send_task(self, task_name: str, task_args: list, task_kwargs: dict, queue_kwargs: Dict[str, Any]) -> str:
209
235
  celery_app = self._get_celery_app()
210
236
 
@@ -237,7 +263,54 @@ class CManager:
237
263
  cm = CManager()
238
264
 
239
265
 
240
- @shared_task(bind=True, max_retries=K_MAX_RETRY_COUNT)
266
+ @shared_task(bind=True, max_retries=K_MAX_RETRY_COUNT, acks_late=True, reject_on_worker_lost=True)
267
+ def callable_executor(self, callable_obj=None, args=None, kwargs=None, retry=None):
268
+ job_id = self.request.id
269
+ try:
270
+ args = args or []
271
+ kwargs = kwargs or {}
272
+ callable_name = getattr(callable_obj, '__name__', str(callable_obj))
273
+
274
+ logger.info(
275
+ f"[TaskQueue] Executing callable: {callable_name} with args: {args} and kwargs: {kwargs}, job_id: {job_id}")
276
+
277
+ callable_obj(*args, **kwargs)
278
+
279
+ logger.info(
280
+ f"[TaskQueue] Callable execution completed successfully, callable: {callable_name}, args: {args}, kwargs: {kwargs}, job_id: {job_id}")
281
+ return None
282
+ except Exception as e:
283
+ logger.exception(
284
+ f"[TaskQueue] Error executing callable: {callable_name}, args: {args}, kwargs: {kwargs}, error_class: {e.__class__.__name__}, error: {e}, job_id: {job_id}")
285
+
286
+ current_retries = getattr(self.request, 'retries', 0) or 0
287
+ max_retries = self.max_retries or K_MAX_RETRY_COUNT
288
+ if isinstance(retry, dict) and 'max_retries' in retry:
289
+ max_retries = retry['max_retries']
290
+
291
+ if current_retries >= max_retries:
292
+ logger.error(
293
+ f"[TaskQueue] Max retries ({max_retries}) reached for callable: {callable_name}, job_id: {job_id}")
294
+ self.update_state(state='FAILURE', meta={
295
+ 'exc_type': type(e).__name__, 'exc_message': str(e)})
296
+
297
+ SlackbotManager.send_message(
298
+ f"Job Failed Too Many Times - Moving back to dlq.\n"
299
+ f"function name: {callable_name}\n"
300
+ f"args: {args}\n"
301
+ f"kwargs: {kwargs}"
302
+ )
303
+
304
+ raise Reject(reason=str(e), requeue=False)
305
+
306
+ countdown = K_DEFAULT_RETRY_COUNTDOWN
307
+ if isinstance(retry, dict) and 'countdown' in retry:
308
+ countdown = retry['countdown']
309
+
310
+ raise self.retry(exc=e, countdown=countdown, max_retries=max_retries)
311
+
312
+
313
+ @shared_task(bind=True, max_retries=K_MAX_RETRY_COUNT, acks_late=True, reject_on_worker_lost=True)
241
314
  def dynamic_function_executor(self, module_path=None, function_name=None, args=None, kwargs=None, retry=None):
242
315
  job_id = self.request.id
243
316
  try:
@@ -261,8 +334,18 @@ def dynamic_function_executor(self, module_path=None, function_name=None, args=N
261
334
 
262
335
  if current_retries >= max_retries:
263
336
  logger.error(
264
- f"[TaskQueue] Max retries ({max_retries}) reached for function: {function_name}, marking task as FAILED, job_id: {job_id}")
265
- raise
337
+ f"[TaskQueue] Max retries ({max_retries}) reached for function: {function_name}, job_id: {job_id}")
338
+ self.update_state(state='FAILURE', meta={
339
+ 'exc_type': type(e).__name__, 'exc_message': str(e)})
340
+
341
+ SlackbotManager.send_message(
342
+ f"Job Failed Too Many Times - Moving back to dlq.\n"
343
+ f"function name: {function_name}\n"
344
+ f"args: {args}\n"
345
+ f"kwargs: {kwargs}"
346
+ )
347
+
348
+ raise Reject(reason=str(e), requeue=False)
266
349
 
267
350
  countdown = K_DEFAULT_RETRY_COUNTDOWN
268
351
  if isinstance(retry, dict) and 'countdown' in retry:
@@ -271,7 +354,7 @@ def dynamic_function_executor(self, module_path=None, function_name=None, args=N
271
354
  raise self.retry(exc=e, countdown=countdown, max_retries=max_retries)
272
355
 
273
356
 
274
- @shared_task(bind=True, max_retries=K_MAX_RETRY_COUNT)
357
+ @shared_task(bind=True, max_retries=K_MAX_RETRY_COUNT, acks_late=True, reject_on_worker_lost=True)
275
358
  def dynamic_class_method_executor(self, module_path=None, class_name=None, method_name=None, args=None, kwargs=None, init_args=None, init_kwargs=None, retry=None):
276
359
  job_id = self.request.id
277
360
  try:
@@ -299,8 +382,18 @@ def dynamic_class_method_executor(self, module_path=None, class_name=None, metho
299
382
 
300
383
  if current_retries >= max_retries:
301
384
  logger.error(
302
- f"[TaskQueue] Max retries ({max_retries}) reached for method: {method_name}, marking task as FAILED, job_id: {job_id}")
303
- raise
385
+ f"[TaskQueue] Max retries ({max_retries}) reached for method: {method_name}, job_id: {job_id}")
386
+ self.update_state(state='FAILURE', meta={
387
+ 'exc_type': type(e).__name__, 'exc_message': str(e)})
388
+
389
+ SlackbotManager.send_message(
390
+ f"Job Failed Too Many Times - Moving back to dlq.\n"
391
+ f"function name: {class_name}.{method_name}\n"
392
+ f"args: {args}\n"
393
+ f"kwargs: {kwargs}"
394
+ )
395
+
396
+ raise Reject(reason=str(e), requeue=False)
304
397
 
305
398
  countdown = K_DEFAULT_RETRY_COUNTDOWN
306
399
  if isinstance(retry, dict) and 'countdown' in retry:
@@ -0,0 +1,51 @@
1
+ """
2
+ Slack notification for TaskQueue.
3
+ """
4
+ import json
5
+ import logging
6
+
7
+ import requests
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class SlackbotManager:
13
+
14
+ @classmethod
15
+ def send_message(cls, message: str) -> None:
16
+ try:
17
+ from django.conf import settings
18
+ except ImportError:
19
+ return
20
+
21
+ if not getattr(settings, 'TASKQUEUE_SLACK_ENABLED', False):
22
+ return
23
+
24
+ hook_url = getattr(settings, 'TASKQUEUE_SLACK_HOOK_URL', None)
25
+ if not hook_url:
26
+ return
27
+
28
+ channel = getattr(
29
+ settings, 'TASKQUEUE_SLACK_CHANNEL_NAME', '#tech-automation')
30
+ username = getattr(
31
+ settings, 'TASKQUEUE_SLACK_USERNAME', 'TaskQueueBot')
32
+ icon_emoji = getattr(
33
+ settings, 'TASKQUEUE_SLACK_ICON_EMOJI', ':robot_face:')
34
+
35
+ is_staging = getattr(settings, 'IS_RUN_IN_STAGING_ENV', False)
36
+ if is_staging:
37
+ message = '[STAGING] ' + message
38
+
39
+ try:
40
+ requests.post(
41
+ hook_url,
42
+ data=json.dumps({
43
+ 'channel': channel,
44
+ 'username': username,
45
+ 'text': message,
46
+ 'icon_emoji': icon_emoji,
47
+ }),
48
+ headers={"Content-Type": "application/json"}
49
+ )
50
+ except Exception as e:
51
+ logger.exception('[TaskQueue Slack] Error: %s', str(e))
@@ -1,9 +0,0 @@
1
- taskqueue/__init__.py,sha256=nNHXIyysNLom9f8of-d__pWJ-YF53Mbrbsb1frPzPPI,298
2
- taskqueue/celery_app.py,sha256=neNjLNDouBwUJsDHn6q3cgQQnmwjBFJ0xTVWHR8cWvY,3482
3
- taskqueue/cmanager.py,sha256=dzYubR9SkMzEYuatt3AL_umkVX_l5BHenz3urp14mLw,12411
4
- taskqueue/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- taskqueue/libs/helper_test.py,sha256=JCdh2S29PpL8RqUxqkcIqwIvr3M9puqHglBZAmfPkuw,7722
6
- p1_taskqueue-0.1.12.dist-info/METADATA,sha256=9NOpVhO1MhpRLe7B96FfLZmgLE8Sn8AIEpXeDsoISHU,1565
7
- p1_taskqueue-0.1.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- p1_taskqueue-0.1.12.dist-info/top_level.txt,sha256=hA3SM1ik2K8iPqtlt_-_nJ4TAePwHPN3vsOc4EiynqU,10
9
- p1_taskqueue-0.1.12.dist-info/RECORD,,