55from collections .abc import Callable
66from functools import wraps
77
8+ from celery import current_task , states
9+ from celery .exceptions import Reject
810from django .core .cache import caches
911
1012log = logging .getLogger (__name__ )
@@ -28,9 +30,9 @@ def cooldown_task(
2830 Place this *below* ``@app.task`` so the cooldown runs on the worker, not
2931 on the enqueuing process.
3032
31- Dropped calls return ``None``. If the wrapped function may itself return
32- ``None``, callers cannot disambiguate — annotate as ``int | None`` (or
33- similar) and treat ``None `` as "skipped" .
33+ When a call is skipped from inside a Celery worker, the task's state is
34+ explicitly set to ``REJECTED`` in the result backend and
35+ ``Reject(requeue=False) `` is raised (should be ignored by sentry) .
3436
3537 To bypass the cooldown for a specific invocation (e.g., operator-forced
3638 recovery), pass ``_cooldown_force=True`` as a kwarg through ``delay()``
@@ -73,6 +75,13 @@ def wrapper(*args, **kwargs):
7375 caches ["redis" ].set (lock_key , "1" , timeout = wait_time )
7476 elif not caches ["redis" ].add (lock_key , "1" , timeout = wait_time ):
7577 log .info ("Skipping %s: cooldown active (%ss)" , lock_key , wait_time )
78+ if current_task and not current_task .request .called_directly :
79+ current_task .update_state (
80+ state = states .REJECTED ,
81+ meta = {"reason" : "cooldown" , "key" : lock_key },
82+ )
83+ reason = "cooldown active"
84+ raise Reject (reason , requeue = False )
7685 return None
7786 return func (* args , ** kwargs )
7887
0 commit comments