mirror of
https://github.com/django-q2/django-q2.git
synced 2026-09-19 10:58:06 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8501bfb4c | ||
|
|
23c06a38eb | ||
|
|
f8321e7f54 | ||
|
|
630b3c62b3 | ||
|
|
227caa7446 | ||
|
|
4ba9e0338a | ||
|
|
fb960104c3 | ||
|
|
88bbfd8967 | ||
|
|
1f31725f43 |
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
|
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
|
||||||
django: [ "3.2", "4.1" ]
|
django: [ "3.2", "4.1", "4.2" ]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
disque:
|
disque:
|
||||||
|
|||||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -2,6 +2,27 @@
|
|||||||
|
|
||||||
## [Unreleased](https://github.com/GDay/django-q2/tree/HEAD)
|
## [Unreleased](https://github.com/GDay/django-q2/tree/HEAD)
|
||||||
|
|
||||||
|
## [v1.5.4](https://github.com/GDay/django-q2/tree/v1.5.4) (2023-06-29)
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Rerun successful tasks https://github.com/django-q2/django-q2/pull/99
|
||||||
|
|
||||||
|
## [v1.5.3](https://github.com/GDay/django-q2/tree/v1.5.3) (2023-05-14)
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Add post_spawn signal. https://github.com/django-q2/django-q2/pull/93
|
||||||
|
- Post spawn docs https://github.com/django-q2/django-q2/pull/95
|
||||||
|
- Make processes identifiable with uuid4 https://github.com/django-q2/django-q2/pull/91
|
||||||
|
|
||||||
|
## [v1.5.2](https://github.com/GDay/django-q2/tree/v1.5.2) (2023-04-13)
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Added Django 4.2 to the test matrix, fixed deprecation warning https://github.com/GDay/django-q2/pull/89
|
||||||
|
- Updated docs to show support for 4.2
|
||||||
|
|
||||||
## [v1.5.1](https://github.com/GDay/django-q2/tree/v1.5.1) (2023-04-02)
|
## [v1.5.1](https://github.com/GDay/django-q2/tree/v1.5.1) (2023-04-02)
|
||||||
|
|
||||||
- Fix release to pipy due to changed org name
|
- Fix release to pipy due to changed org name
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ Requirements
|
|||||||
- `Django <https://www.djangoproject.com>`__ > = 3.2
|
- `Django <https://www.djangoproject.com>`__ > = 3.2
|
||||||
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__
|
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__
|
||||||
|
|
||||||
Tested with: Python 3.8, 3.9, 3.10, 3.11 Django 3.2.X and 4.1.X
|
Tested with: Python 3.8, 3.9, 3.10 and 3.11. Works with Django 3.2.X, 4.1.X and 4.2.X.
|
||||||
|
|
||||||
Brokers
|
Brokers
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import django
|
import django
|
||||||
|
|
||||||
VERSION = (1, 5, 1)
|
VERSION = (1, 5, 4)
|
||||||
|
|
||||||
if django.VERSION < (3, 2):
|
if django.VERSION < (3, 2):
|
||||||
default_app_config = "django_q.apps.DjangoQConfig"
|
default_app_config = "django_q.apps.DjangoQConfig"
|
||||||
|
|||||||
@@ -10,10 +10,29 @@ from django_q.models import Failure, OrmQ, Schedule, Success, Task
|
|||||||
from django_q.tasks import async_task
|
from django_q.tasks import async_task
|
||||||
|
|
||||||
|
|
||||||
|
def resubmit_task(model_admin, request, queryset):
|
||||||
|
"""Submit selected tasks back to the queue."""
|
||||||
|
for task in queryset:
|
||||||
|
async_task(
|
||||||
|
task.func,
|
||||||
|
*task.args or (),
|
||||||
|
hook=task.hook,
|
||||||
|
group=task.group,
|
||||||
|
cluster=task.cluster,
|
||||||
|
**task.kwargs or {},
|
||||||
|
)
|
||||||
|
if isinstance(model_admin, FailAdmin):
|
||||||
|
task.delete()
|
||||||
|
|
||||||
|
|
||||||
|
resubmit_task.short_description = _("Resubmit selected tasks to queue")
|
||||||
|
|
||||||
|
|
||||||
class TaskAdmin(admin.ModelAdmin):
|
class TaskAdmin(admin.ModelAdmin):
|
||||||
"""model admin for success tasks."""
|
"""model admin for success tasks."""
|
||||||
|
|
||||||
list_display = ("name", "group", "func", "cluster", "started", "stopped", "time_taken")
|
list_display = ("name", "group", "func", "cluster", "started", "stopped", "time_taken")
|
||||||
|
actions = [resubmit_task]
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
"""Don't allow adds."""
|
"""Don't allow adds."""
|
||||||
@@ -33,17 +52,6 @@ class TaskAdmin(admin.ModelAdmin):
|
|||||||
return list(self.readonly_fields) + [field.name for field in obj._meta.fields]
|
return list(self.readonly_fields) + [field.name for field in obj._meta.fields]
|
||||||
|
|
||||||
|
|
||||||
def retry_failed(FailAdmin, request, queryset):
|
|
||||||
"""Submit selected tasks back to the queue."""
|
|
||||||
for task in queryset:
|
|
||||||
async_task(task.func, *task.args or (), hook=task.hook,
|
|
||||||
group=task.group, cluster=task.cluster, **task.kwargs or {})
|
|
||||||
task.delete()
|
|
||||||
|
|
||||||
|
|
||||||
retry_failed.short_description = _("Resubmit selected tasks to queue")
|
|
||||||
|
|
||||||
|
|
||||||
class FailAdmin(admin.ModelAdmin):
|
class FailAdmin(admin.ModelAdmin):
|
||||||
"""model admin for failed tasks."""
|
"""model admin for failed tasks."""
|
||||||
|
|
||||||
@@ -53,7 +61,7 @@ class FailAdmin(admin.ModelAdmin):
|
|||||||
"""Don't allow adds."""
|
"""Don't allow adds."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
actions = [retry_failed]
|
actions = [resubmit_task]
|
||||||
search_fields = ("name", "func", "group")
|
search_fields = ("name", "func", "group")
|
||||||
list_filter = ("group", "cluster")
|
list_filter = ("group", "cluster")
|
||||||
readonly_fields = []
|
readonly_fields = []
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ from django_q.conf import (
|
|||||||
from django_q.humanhash import humanize
|
from django_q.humanhash import humanize
|
||||||
from django_q.models import Schedule, Success, Task
|
from django_q.models import Schedule, Success, Task
|
||||||
from django_q.queues import Queue
|
from django_q.queues import Queue
|
||||||
from django_q.signals import post_execute, pre_execute
|
from django_q.signals import post_execute, post_spawn, pre_execute
|
||||||
from django_q.signing import BadSignature, SignedPackage
|
from django_q.signing import BadSignature, SignedPackage
|
||||||
from django_q.status import Stat, Status
|
from django_q.status import Stat, Status
|
||||||
|
|
||||||
@@ -69,6 +69,7 @@ class Cluster:
|
|||||||
self.start_event = Event()
|
self.start_event = Event()
|
||||||
self.sentinel = Process(
|
self.sentinel = Process(
|
||||||
target=Sentinel,
|
target=Sentinel,
|
||||||
|
name=f"Process-{uuid.uuid4().hex}",
|
||||||
args=(
|
args=(
|
||||||
self.stop_event,
|
self.stop_event,
|
||||||
self.start_event,
|
self.start_event,
|
||||||
@@ -196,7 +197,7 @@ class Sentinel:
|
|||||||
"""
|
"""
|
||||||
:type target: function or class
|
:type target: function or class
|
||||||
"""
|
"""
|
||||||
p = Process(target=target, args=args)
|
p = Process(target=target, args=args, name=f"Process-{uuid.uuid4().hex}")
|
||||||
p.daemon = True
|
p.daemon = True
|
||||||
if target == worker:
|
if target == worker:
|
||||||
p.daemon = Conf.DAEMONIZE_WORKERS
|
p.daemon = Conf.DAEMONIZE_WORKERS
|
||||||
@@ -481,6 +482,7 @@ def worker(
|
|||||||
_("%(proc_name)s ready for work at %(id)s")
|
_("%(proc_name)s ready for work at %(id)s")
|
||||||
% {"proc_name": proc_name, "id": current_process().pid}
|
% {"proc_name": proc_name, "id": current_process().pid}
|
||||||
)
|
)
|
||||||
|
post_spawn.send(sender="django_q", proc_name=proc_name)
|
||||||
if setproctitle:
|
if setproctitle:
|
||||||
setproctitle.setproctitle(f"qcluster {proc_name} idle")
|
setproctitle.setproctitle(f"qcluster {proc_name} idle")
|
||||||
task_count = 0
|
task_count = 0
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def loads(
|
|||||||
"""
|
"""
|
||||||
# TimestampSigner.unsign() returns str but base64 and zlib compression
|
# TimestampSigner.unsign() returns str but base64 and zlib compression
|
||||||
# operate on bytes.
|
# operate on bytes.
|
||||||
base64d = force_bytes(TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
|
base64d = force_bytes(TimestampSigner(key=key, salt=salt).unsign(s, max_age=max_age))
|
||||||
decompress = False
|
decompress = False
|
||||||
if base64d[:1] == b".":
|
if base64d[:1] == b".":
|
||||||
# It's compressed; uncompress it first
|
# It's compressed; uncompress it first
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ def call_hook(sender, instance, **kwargs):
|
|||||||
% {"hook": instance.hook, "name": instance.name, "error": str(e)}
|
% {"hook": instance.hook, "name": instance.name, "error": str(e)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# args: proc_name
|
||||||
|
post_spawn = Signal()
|
||||||
|
|
||||||
# args: task
|
# args: task
|
||||||
pre_enqueue = Signal()
|
pre_enqueue = Signal()
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ def test_admin_views(admin_client, monkeypatch):
|
|||||||
|
|
||||||
# resubmit the failure
|
# resubmit the failure
|
||||||
url = reverse("admin:django_q_failure_changelist")
|
url = reverse("admin:django_q_failure_changelist")
|
||||||
data = {"action": "retry_failed", "_selected_action": [f.pk]}
|
data = {"action": "resubmit_task", "_selected_action": [f.pk]}
|
||||||
response = admin_client.post(url, data)
|
response = admin_client.post(url, data)
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
assert Failure.objects.filter(name=f.id).exists() is False
|
assert Failure.objects.filter(name=f.id).exists() is False
|
||||||
@@ -84,3 +84,10 @@ def test_admin_views(admin_client, monkeypatch):
|
|||||||
data = {"post": "yes"}
|
data = {"post": "yes"}
|
||||||
response = admin_client.post(url, data)
|
response = admin_client.post(url, data)
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
|
# Resubmit a successful task.
|
||||||
|
url = reverse("admin:django_q_success_changelist")
|
||||||
|
data = {"action": "resubmit_task", "_selected_action": [t.pk]}
|
||||||
|
initial_queue_count = OrmQ.objects.count()
|
||||||
|
response = admin_client.post(url, data)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert OrmQ.objects.count() > initial_queue_count
|
||||||
|
|||||||
@@ -11,36 +11,36 @@ Start your cluster using Django's ``manage.py`` command::
|
|||||||
|
|
||||||
You should see the cluster starting ::
|
You should see the cluster starting ::
|
||||||
|
|
||||||
10:57:40 [Q] INFO Q Cluster-31781 starting.
|
10:57:40 [Q] INFO Q Cluster freddie-uncle-twenty-ten starting.
|
||||||
10:57:40 [Q] INFO Process-1:1 ready for work at 31784
|
10:57:40 [Q] INFO Process-ede257774c4444c980ab479f10947acc ready for work at 31784
|
||||||
10:57:40 [Q] INFO Process-1:2 ready for work at 31785
|
10:57:40 [Q] INFO Process-ed580482da3f42968230baa2e4253e42 ready for work at 31785
|
||||||
10:57:40 [Q] INFO Process-1:3 ready for work at 31786
|
10:57:40 [Q] INFO Process-8a370dc2bc1d49aa9864e517c9895f74 ready for work at 31786
|
||||||
10:57:40 [Q] INFO Process-1:4 ready for work at 31787
|
10:57:40 [Q] INFO Process-74912f9844264d1397c6e54476b530c0 ready for work at 31787
|
||||||
10:57:40 [Q] INFO Process-1:5 ready for work at 31788
|
10:57:40 [Q] INFO Process-b00edb26c6074a6189e5696c60aeb35b ready for work at 31788
|
||||||
10:57:40 [Q] INFO Process-1:6 ready for work at 31789
|
10:57:40 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 ready for work at 31789
|
||||||
10:57:40 [Q] INFO Process-1:7 ready for work at 31790
|
10:57:40 [Q] INFO Process-7e8abbb8ca2d4d9bb20a937dd5e2872b ready for work at 31790
|
||||||
10:57:40 [Q] INFO Process-1:8 ready for work at 31791
|
10:57:40 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 ready for work at 31791
|
||||||
10:57:40 [Q] INFO Process-1:9 monitoring at 31792
|
10:57:40 [Q] INFO Process-67fa9461ac034736a766cd813f617e62 monitoring at 31792
|
||||||
10:57:40 [Q] INFO Process-1 guarding cluster at 31783
|
10:57:40 [Q] INFO Process-eac052c646b2459797cee98bdb84c85d guarding cluster at 31783
|
||||||
10:57:40 [Q] INFO Process-1:10 pushing tasks at 31793
|
10:57:40 [Q] INFO Process-5d98deb19b1e4b2da2ef1e5bd6824f75 pushing tasks at 31793
|
||||||
10:57:40 [Q] INFO Q Cluster-31781 running.
|
10:57:40 [Q] INFO Q Cluster freddie-uncle-twenty-ten running.
|
||||||
|
|
||||||
|
|
||||||
Stopping the cluster with ctrl-c or either the ``SIGTERM`` and ``SIGKILL`` signals, will initiate the :ref:`stop_procedure`::
|
Stopping the cluster with ctrl-c or either the ``SIGTERM`` and ``SIGKILL`` signals, will initiate the :ref:`stop_procedure`::
|
||||||
|
|
||||||
16:44:12 [Q] INFO Q Cluster-31781 stopping.
|
16:44:12 [Q] INFO Q Cluster freddie-uncle-twenty-ten stopping.
|
||||||
16:44:12 [Q] INFO Process-1 stopping cluster processes
|
16:44:12 [Q] INFO Process-eac052c646b2459797cee98bdb84c85d stopping cluster processes
|
||||||
16:44:13 [Q] INFO Process-1:10 stopped pushing tasks
|
16:44:13 [Q] INFO Process-5d98deb19b1e4b2da2ef1e5bd6824f75 stopped pushing tasks
|
||||||
16:44:13 [Q] INFO Process-1:6 stopped doing work
|
16:44:13 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 stopped doing work
|
||||||
16:44:13 [Q] INFO Process-1:4 stopped doing work
|
16:44:13 [Q] INFO Process-7e8abbb8ca2d4d9bb20a937dd5e2872b stopped doing work
|
||||||
16:44:13 [Q] INFO Process-1:1 stopped doing work
|
16:44:13 [Q] INFO Process-b0862965db04479f9784a26639ee51e0 stopped doing work
|
||||||
16:44:13 [Q] INFO Process-1:5 stopped doing work
|
16:44:13 [Q] INFO Process-b00edb26c6074a6189e5696c60aeb35b stopped doing work
|
||||||
16:44:13 [Q] INFO Process-1:7 stopped doing work
|
16:44:13 [Q] INFO Process-74912f9844264d1397c6e54476b530c0 stopped doing work
|
||||||
16:44:13 [Q] INFO Process-1:3 stopped doing work
|
16:44:13 [Q] INFO Process-8a370dc2bc1d49aa9864e517c9895f74 stopped doing work
|
||||||
16:44:13 [Q] INFO Process-1:8 stopped doing work
|
16:44:13 [Q] INFO Process-ed580482da3f42968230baa2e4253e42 stopped doing work
|
||||||
16:44:13 [Q] INFO Process-1:2 stopped doing work
|
16:44:13 [Q] INFO Process-ede257774c4444c980ab479f10947acc stopped doing work
|
||||||
16:44:14 [Q] INFO Process-1:9 stopped monitoring results
|
16:44:14 [Q] INFO Process-67fa9461ac034736a766cd813f617e62 stopped monitoring results
|
||||||
16:44:15 [Q] INFO Q Cluster-31781 has stopped.
|
16:44:15 [Q] INFO Q Cluster freddie-uncle-twenty-ten has stopped.
|
||||||
|
|
||||||
The number of workers, optional timeouts, recycles and cpu_affinity can be controlled via the :doc:`configure` settings.
|
The number of workers, optional timeouts, recycles and cpu_affinity can be controlled via the :doc:`configure` settings.
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ author = "Ilan Steemers, Stan Triepels"
|
|||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = "1.5"
|
version = "1.5"
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = "1.5.1"
|
release = "1.5.4"
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Features
|
|||||||
- Rollbar and Sentry support
|
- Rollbar and Sentry support
|
||||||
|
|
||||||
|
|
||||||
Django Q2 is tested with: Python 3.8, 3.9 and 3.10, 3.11, Django 3.2.x and 4.1.x
|
Django Q2 is tested with: Python 3.8, 3.9, 3.10 and 3.11. Works with Django 3.2.x, 4.1.x and 4.2.x
|
||||||
|
|
||||||
Currently available in English, German and French.
|
Currently available in English, German and French.
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Django Q2 is tested for Python 3.8, 3.9, 3.10 and 3.11
|
|||||||
- `Django <https://www.djangoproject.com>`__
|
- `Django <https://www.djangoproject.com>`__
|
||||||
|
|
||||||
Django Q2 aims to use as much of Django's standard offerings as possible.
|
Django Q2 aims to use as much of Django's standard offerings as possible.
|
||||||
The code is tested against Django versions `3.2.x` and `4.1.x`.
|
The code is tested against Django versions `3.2.x`, `4.1.x` and`4.2.x`.
|
||||||
|
|
||||||
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__
|
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ You can reference the `requirements <https://github.com/GDay/django-q2/blob/mast
|
|||||||
Django
|
Django
|
||||||
~~~~~~
|
~~~~~~
|
||||||
We strive to be compatible with last two major version of Django.
|
We strive to be compatible with last two major version of Django.
|
||||||
At the moment this means we support the 3.2.x and 4.1.x releases.
|
At the moment this means we support the 3.2.x, 4.1.x and 4.2.x releases.
|
||||||
|
|
||||||
Since we are now no longer supporting Python 2, we can also not support older versions of Django that do not support Python >= 3.6
|
Since we are now no longer supporting Python 2, we can also not support older versions of Django that do not support Python >= 3.6
|
||||||
For this you can always use older releases, but they are no longer maintained.
|
For this you can always use older releases, but they are no longer maintained.
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ Before enqueuing a task
|
|||||||
The ``django_q.signals.pre_enqueue`` signal is emitted before a task is
|
The ``django_q.signals.pre_enqueue`` signal is emitted before a task is
|
||||||
enqueued. The task dictionary is given as the ``task`` argument.
|
enqueued. The task dictionary is given as the ``task`` argument.
|
||||||
|
|
||||||
|
After spawning a worker process
|
||||||
|
"""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
The ``django_q.signals.post_spawn`` signal is emitted after a worker process has
|
||||||
|
spawned. The process name is given as the ``proc_name`` argument (string).
|
||||||
|
|
||||||
Before executing a task
|
Before executing a task
|
||||||
"""""""""""""""""""""""
|
"""""""""""""""""""""""
|
||||||
|
|
||||||
@@ -37,7 +43,7 @@ Connecting to a Django Q2 signal is done the same as any other Django
|
|||||||
signal::
|
signal::
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django_q.signals import pre_enqueue, pre_execute, post_execute
|
from django_q.signals import pre_enqueue, pre_execute, post_execute, post_spawn
|
||||||
|
|
||||||
@receiver(pre_enqueue)
|
@receiver(pre_enqueue)
|
||||||
def my_pre_enqueue_callback(sender, task, **kwargs):
|
def my_pre_enqueue_callback(sender, task, **kwargs):
|
||||||
@@ -51,4 +57,8 @@ signal::
|
|||||||
def my_post_execute_callback(sender, task, **kwargs):
|
def my_post_execute_callback(sender, task, **kwargs):
|
||||||
print(f"Task {task['name']} was executed with result {task['result']}")
|
print(f"Task {task['name']} was executed with result {task['result']}")
|
||||||
|
|
||||||
|
@receiver(post_spawn)
|
||||||
|
def my_post_spawn_callback(sender, proc_name, **kwargs):
|
||||||
|
print(f"Process {proc_name} has spawned")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "django-q2"
|
name = "django-q2"
|
||||||
version = "1.5.1"
|
version = "1.5.4"
|
||||||
packages = [
|
packages = [
|
||||||
{ include = "django_q" },
|
{ include = "django_q" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user