django-simpletask5 0.2.1__tar.gz → 0.2.2__tar.gz

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.
Files changed (59) hide show
  1. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/PKG-INFO +59 -3
  2. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/README.md +57 -2
  3. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/__init__.py +1 -0
  4. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/admin.py +31 -14
  5. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/core/cronjob_registry.py +19 -6
  6. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/core/defaults.py +17 -2
  7. django_simpletask5-0.2.2/django_simpletask5/core/lock.py +22 -0
  8. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/core/message_queue.py +23 -25
  9. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/core/publisher.py +47 -0
  10. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/core/worker_registry.py +5 -2
  11. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/dashboards.py +28 -22
  12. django_simpletask5-0.2.2/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  13. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.po +68 -36
  14. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/management/commands/django_simpletask_crontab.py +2 -2
  15. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/management/commands/django_simpletask_executor.py +142 -70
  16. django_simpletask5-0.2.2/django_simpletask5/migrations/0009_add_cronjob_app_label.py +20 -0
  17. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/models.py +45 -0
  18. django_simpletask5-0.2.2/django_simpletask5/static/django_simpletask5/css/dashboard.css +16 -0
  19. django_simpletask5-0.2.2/django_simpletask5/static/django_simpletask5/js/select2_executor.js +89 -0
  20. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5.egg-info/PKG-INFO +59 -3
  21. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5.egg-info/SOURCES.txt +4 -1
  22. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5.egg-info/requires.txt +1 -0
  23. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/pyproject.toml +3 -2
  24. django_simpletask5-0.2.1/django_simpletask5/core/lock.py +0 -24
  25. django_simpletask5-0.2.1/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  26. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/LICENSE +0 -0
  27. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/apps.py +0 -0
  28. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/core/__init__.py +0 -0
  29. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/core/executor_scanner.py +0 -0
  30. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/core/signals.py +0 -0
  31. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/__init__.py +0 -0
  32. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/archive.py +0 -0
  33. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/base.py +0 -0
  34. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/bash_script.py +0 -0
  35. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/exceptions.py +0 -0
  36. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/loader.py +0 -0
  37. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/ping_pong.py +0 -0
  38. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/python_script.py +0 -0
  39. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/retry_timeout.py +0 -0
  40. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/simple_request.py +0 -0
  41. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/executors/status_check.py +0 -0
  42. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/management/__init__.py +0 -0
  43. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/management/commands/__init__.py +0 -0
  44. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/management/commands/django_simpletask_archive.py +0 -0
  45. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/management/commands/django_simpletask_sync_cronjobs.py +0 -0
  46. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/migrations/0001_initial.py +0 -0
  47. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/migrations/0002_taskexecution_done_tasks_count_and_more.py +0 -0
  48. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/migrations/0003_alter_cronjob_options_remove_cronjob_name_and_more.py +0 -0
  49. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/migrations/0004_alter_cronjob_is_modified_by_user.py +0 -0
  50. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/migrations/0005_alter_cronjob_is_modified_by_user_help.py +0 -0
  51. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/migrations/0006_alter_cronjob_options_alter_taskexecution_created_at_and_more.py +0 -0
  52. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/migrations/0007_rename_task_model_to_executor_class_in_stat.py +0 -0
  53. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/migrations/0008_taskexecution_pickup_timeout_seconds.py +0 -0
  54. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/migrations/__init__.py +0 -0
  55. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/services/__init__.py +0 -0
  56. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5/services/archive.py +0 -0
  57. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5.egg-info/dependency_links.txt +0 -0
  58. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/django_simpletask5.egg-info/top_level.txt +0 -0
  59. {django_simpletask5-0.2.1 → django_simpletask5-0.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-simpletask5
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Lightweight async task execution framework for Django
5
5
  Author-email: rRR0VrFP <rrr0vrfp@qq.com>
6
6
  Maintainer-email: rRR0VrFP <rrr0vrfp@qq.com>
@@ -29,6 +29,7 @@ Requires-Dist: django-app-requires>=0.3.6
29
29
  Requires-Dist: django-admin-dashboards>=0.1.1
30
30
  Requires-Dist: django-checkbox-normalize>=0.2.2
31
31
  Requires-Dist: django-tabbed-changeform-admin>=0.1.9
32
+ Requires-Dist: django-static-fontawesome>=5.0.2
32
33
  Requires-Dist: cryptography>=48.0.0
33
34
  Requires-Dist: croniter>=6.2.2
34
35
  Requires-Dist: requests>=2.34.2
@@ -135,6 +136,19 @@ DJANGO_SIMPLETASK_RABBITMQ_PASSWORD = 'guest'
135
136
 
136
137
  ### 分布式锁
137
138
 
139
+ 默认使用 Django 缓存中的 Redis 连接,只需配置好 Django 的 `CACHES`:
140
+
141
+ ```python
142
+ CACHES = {
143
+ 'default': {
144
+ 'BACKEND': 'django_redis.cache.RedisCache',
145
+ 'LOCATION': 'redis://127.0.0.1:6379/1',
146
+ },
147
+ }
148
+ ```
149
+
150
+ 如需自定义锁引擎或连接参数:
151
+
138
152
  ```python
139
153
  DJANGO_SIMPLETASK_LOCK_CONFIG = {
140
154
  'global_lock_engine_class': 'globallock.redis_global_lock.RedisGlobalLock',
@@ -150,8 +164,8 @@ DJANGO_SIMPLETASK_LOCK_TIMEOUT = 600 # 锁超时时间(秒)
150
164
  ### 队列路由
151
165
 
152
166
  ```python
153
- DJANGO_SIMPLETASK_DEFAULT_QUEUE = 'django_simpletask5.queue.default' # 默认队列
154
- DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = 'django_simpletask5.queue.high_priority' # 高优先级队列
167
+ DJANGO_SIMPLETASK_DEFAULT_QUEUE = 'myproject:django-simpletask5:queue:default' # 默认队列
168
+ DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = 'myproject:django-simpletask5:queue:high_priority' # 高优先级队列
155
169
  ```
156
170
 
157
171
  ### 执行与重试
@@ -296,6 +310,31 @@ order = OrderTask.objects.get(order_id='ORD-001')
296
310
  order.trigger('refund', extra_context={'refund_amount': '50.00'})
297
311
  ```
298
312
 
313
+ ### 6. 抑制异步消息
314
+
315
+ 创建或更新 Task 时,可通过 `_skip_publish` 机制禁止触发 RabbitMQ 消息通知,不会创建 `TaskExecution` 也不发送异步消息。提供四种使用方式:
316
+
317
+ ```python
318
+ # 方式一:设置实例属性
319
+ order = OrderTask(order_id='ORD-001', customer_name='Test', amount=Decimal('100.00'))
320
+ order._skip_publish = True
321
+ order.save()
322
+
323
+ # 方式二:调用 skip_publish() 方法(支持链式调用)
324
+ order.skip_publish().save()
325
+
326
+ # 方式三:save 时传入 skip_publish=True
327
+ order.save(skip_publish=True)
328
+
329
+ # 方式四:构造或 create 时直接传入(推荐)
330
+ OrderTask.objects.create(
331
+ order_id='ORD-001', customer_name='Test', amount=Decimal('100.00'),
332
+ skip_publish=True,
333
+ )
334
+ ```
335
+
336
+ `_skip_publish` 是临时实例属性,不会持久化到数据库,后续正常操作不受影响。
337
+
299
338
  ## Cron 任务
300
339
 
301
340
  ### 在代码中定义 Cron 任务
@@ -353,6 +392,23 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
353
392
 
354
393
  ## Releases
355
394
 
395
+ ### 0.2.2
396
+
397
+ - **抑制异步消息** — 新增 `_skip_publish` 机制,创建/更新/删除 Task 时可通过 `skip_publish()` 方法、`save(skip_publish=True)`、构造时传入 `skip_publish=True` 等方式禁止触发 RabbitMQ 消息通知
398
+ - **批量创建触发异步消息** — `TaskQuerySet` + `TaskManager` 接管 `bulk_create`,批量创建 Task 后自动生成 TaskExecution 并推送 MQ;子类覆盖 `objects` 时发出警告提示
399
+ - **分布式锁默认引擎** — 默认使用 `DjangoRedisGlobalLock`,复用 Django `CACHES['default']` 配置(需为 Redis 缓存),无需手动配置 `DJANGO_SIMPLETASK_LOCK_CONFIG`
400
+ - **Redis key 命名统一** — 所有 Redis key 格式规范为 `{项目前缀}:django-simpletask5:{用途}:{id}`,项目前缀从 `ROOT_URLCONF` 自动提取,也可通过 `DJANGO_SIMPLETASK_KEY_PREFIX` 显式覆盖
401
+ - **CronJob 管理增强** — 新增 `app_label` 字段(自动检测),列表页展示并支持过滤
402
+ - **内置 CronJob 重命名** — Status Check → Task Timeout Detection,Retry Timeout → Retry Failed Tasks,Ping/Pong → Health Check 等
403
+ - **Dashboard 重做** — 新增今日失败卡片,成功率按 90%/80% 分绿色/橙色/红色三级,卡片颜色统一使用 info 蓝色
404
+ - **Pending/Running 显示修复** — 改为展示 `pending / running` 双值而非仅 pending
405
+ - **成功率显示优化** — `(22/25)` 与 `88%` 同行显示,字号缩小颜色淡化,高度对齐
406
+ - **Redis 不可达降级** — Worker 数量显示 `-` 而非 `0`,区分无数据与异常
407
+ - **按钮优化** — 改用 Font Awesome 图标,统一 `min-width` 对齐,title 改为中文描述
408
+ - **翻译补充** — 补充 Run/Activate/Deactivate 及所有内置 CronJob 名称的中文翻译
409
+ - **新增依赖** — `django-static-fontawesome` 用于按钮图标
410
+ - **示例应用** — 示例 cronjobs 模块在 AppConfig.ready() 中导入确保注册生效
411
+
356
412
  ### 0.2.1
357
413
 
358
414
  - **拒绝回退机制** — 新增 `RejectException`,执行器可抛出该异常拒绝当前任务,消息自动重新入队等待下次调度,不消耗重试次数
@@ -96,6 +96,19 @@ DJANGO_SIMPLETASK_RABBITMQ_PASSWORD = 'guest'
96
96
 
97
97
  ### 分布式锁
98
98
 
99
+ 默认使用 Django 缓存中的 Redis 连接,只需配置好 Django 的 `CACHES`:
100
+
101
+ ```python
102
+ CACHES = {
103
+ 'default': {
104
+ 'BACKEND': 'django_redis.cache.RedisCache',
105
+ 'LOCATION': 'redis://127.0.0.1:6379/1',
106
+ },
107
+ }
108
+ ```
109
+
110
+ 如需自定义锁引擎或连接参数:
111
+
99
112
  ```python
100
113
  DJANGO_SIMPLETASK_LOCK_CONFIG = {
101
114
  'global_lock_engine_class': 'globallock.redis_global_lock.RedisGlobalLock',
@@ -111,8 +124,8 @@ DJANGO_SIMPLETASK_LOCK_TIMEOUT = 600 # 锁超时时间(秒)
111
124
  ### 队列路由
112
125
 
113
126
  ```python
114
- DJANGO_SIMPLETASK_DEFAULT_QUEUE = 'django_simpletask5.queue.default' # 默认队列
115
- DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = 'django_simpletask5.queue.high_priority' # 高优先级队列
127
+ DJANGO_SIMPLETASK_DEFAULT_QUEUE = 'myproject:django-simpletask5:queue:default' # 默认队列
128
+ DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = 'myproject:django-simpletask5:queue:high_priority' # 高优先级队列
116
129
  ```
117
130
 
118
131
  ### 执行与重试
@@ -257,6 +270,31 @@ order = OrderTask.objects.get(order_id='ORD-001')
257
270
  order.trigger('refund', extra_context={'refund_amount': '50.00'})
258
271
  ```
259
272
 
273
+ ### 6. 抑制异步消息
274
+
275
+ 创建或更新 Task 时,可通过 `_skip_publish` 机制禁止触发 RabbitMQ 消息通知,不会创建 `TaskExecution` 也不发送异步消息。提供四种使用方式:
276
+
277
+ ```python
278
+ # 方式一:设置实例属性
279
+ order = OrderTask(order_id='ORD-001', customer_name='Test', amount=Decimal('100.00'))
280
+ order._skip_publish = True
281
+ order.save()
282
+
283
+ # 方式二:调用 skip_publish() 方法(支持链式调用)
284
+ order.skip_publish().save()
285
+
286
+ # 方式三:save 时传入 skip_publish=True
287
+ order.save(skip_publish=True)
288
+
289
+ # 方式四:构造或 create 时直接传入(推荐)
290
+ OrderTask.objects.create(
291
+ order_id='ORD-001', customer_name='Test', amount=Decimal('100.00'),
292
+ skip_publish=True,
293
+ )
294
+ ```
295
+
296
+ `_skip_publish` 是临时实例属性,不会持久化到数据库,后续正常操作不受影响。
297
+
260
298
  ## Cron 任务
261
299
 
262
300
  ### 在代码中定义 Cron 任务
@@ -314,6 +352,23 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
314
352
 
315
353
  ## Releases
316
354
 
355
+ ### 0.2.2
356
+
357
+ - **抑制异步消息** — 新增 `_skip_publish` 机制,创建/更新/删除 Task 时可通过 `skip_publish()` 方法、`save(skip_publish=True)`、构造时传入 `skip_publish=True` 等方式禁止触发 RabbitMQ 消息通知
358
+ - **批量创建触发异步消息** — `TaskQuerySet` + `TaskManager` 接管 `bulk_create`,批量创建 Task 后自动生成 TaskExecution 并推送 MQ;子类覆盖 `objects` 时发出警告提示
359
+ - **分布式锁默认引擎** — 默认使用 `DjangoRedisGlobalLock`,复用 Django `CACHES['default']` 配置(需为 Redis 缓存),无需手动配置 `DJANGO_SIMPLETASK_LOCK_CONFIG`
360
+ - **Redis key 命名统一** — 所有 Redis key 格式规范为 `{项目前缀}:django-simpletask5:{用途}:{id}`,项目前缀从 `ROOT_URLCONF` 自动提取,也可通过 `DJANGO_SIMPLETASK_KEY_PREFIX` 显式覆盖
361
+ - **CronJob 管理增强** — 新增 `app_label` 字段(自动检测),列表页展示并支持过滤
362
+ - **内置 CronJob 重命名** — Status Check → Task Timeout Detection,Retry Timeout → Retry Failed Tasks,Ping/Pong → Health Check 等
363
+ - **Dashboard 重做** — 新增今日失败卡片,成功率按 90%/80% 分绿色/橙色/红色三级,卡片颜色统一使用 info 蓝色
364
+ - **Pending/Running 显示修复** — 改为展示 `pending / running` 双值而非仅 pending
365
+ - **成功率显示优化** — `(22/25)` 与 `88%` 同行显示,字号缩小颜色淡化,高度对齐
366
+ - **Redis 不可达降级** — Worker 数量显示 `-` 而非 `0`,区分无数据与异常
367
+ - **按钮优化** — 改用 Font Awesome 图标,统一 `min-width` 对齐,title 改为中文描述
368
+ - **翻译补充** — 补充 Run/Activate/Deactivate 及所有内置 CronJob 名称的中文翻译
369
+ - **新增依赖** — `django-static-fontawesome` 用于按钮图标
370
+ - **示例应用** — 示例 cronjobs 模块在 AppConfig.ready() 中导入确保注册生效
371
+
317
372
  ### 0.2.1
318
373
 
319
374
  - **拒绝回退机制** — 新增 `RejectException`,执行器可抛出该异常拒绝当前任务,消息自动重新入队等待下次调度,不消耗重试次数
@@ -3,6 +3,7 @@ default_app_config = 'django_simpletask5.apps.DjangoSimpletask5Config'
3
3
  app_requires = [
4
4
  'django_admin_dashboards',
5
5
  'django_safe_fields',
6
+ 'django_static_fontawesome',
6
7
  'django_tabbed_changeform_admin',
7
8
  'django_checkbox_normalize',
8
9
  ]
@@ -3,6 +3,7 @@ import json
3
3
  from django import forms
4
4
  from django.contrib import admin
5
5
  from django.http import HttpResponseRedirect, Http404
6
+ from django.middleware.csrf import get_token
6
7
  from django.urls import path, reverse
7
8
  from django.utils.html import format_html, mark_safe
8
9
  from django.utils.translation import gettext_lazy as _
@@ -225,6 +226,7 @@ class CronJobAdmin(DjangoCheckboxNormalizeAdmin, DjangoTabbedChangeformAdmin):
225
226
  form = CronJobForm
226
227
  list_display = [
227
228
  "display_name",
229
+ "app_label",
228
230
  "cron_expression",
229
231
  "is_active",
230
232
  "is_modified_by_user",
@@ -232,9 +234,14 @@ class CronJobAdmin(DjangoCheckboxNormalizeAdmin, DjangoTabbedChangeformAdmin):
232
234
  "action_buttons",
233
235
  ]
234
236
  readonly_fields = ["created_at", "updated_at"]
235
- list_filter = ["is_active", "is_modified_by_user"]
237
+ list_filter = ["is_active", "is_modified_by_user", "app_label"]
236
238
  search_fields = ["display_name", "uid", "executor_class"]
237
239
 
240
+ class Media:
241
+ css = {
242
+ "all": ("fontawesome/css/all.min.css",),
243
+ }
244
+
238
245
  fieldsets = [
239
246
  (None, {
240
247
  "fields": ["uid", "display_name", "executor_class", "cron_expression", "description", "created_at", "updated_at"],
@@ -261,6 +268,10 @@ class CronJobAdmin(DjangoCheckboxNormalizeAdmin, DjangoTabbedChangeformAdmin):
261
268
  def get_queryset(self, request):
262
269
  return super().get_queryset(request)
263
270
 
271
+ def changelist_view(self, request, extra_context=None):
272
+ self._admin_request = request
273
+ return super().changelist_view(request, extra_context=extra_context)
274
+
264
275
  def get_urls(self):
265
276
  urls = super().get_urls()
266
277
  custom_urls = [
@@ -311,39 +322,45 @@ class CronJobAdmin(DjangoCheckboxNormalizeAdmin, DjangoTabbedChangeformAdmin):
311
322
  reverse('admin:django_simpletask5_cronjob_changelist')
312
323
  )
313
324
 
325
+ _btn_base = "padding:3px 8px;border-radius:3px;border:none;cursor:pointer;margin-right:4px;font-size:12px;min-width:68px"
326
+
327
+ def _csrf_token_input(self):
328
+ token = get_token(self._admin_request) if hasattr(self, '_admin_request') else ''
329
+ return f'<input type="hidden" name="csrfmiddlewaretoken" value="{token}">'
330
+
314
331
  def action_buttons(self, obj):
315
332
  run_url = reverse('admin:django_simpletask5_cronjob_run', args=[obj.pk])
316
333
  toggle_url = reverse('admin:django_simpletask5_cronjob_toggle_active', args=[obj.pk])
317
- csrf_token = '"{{ csrf_token }}"'
334
+ csrf_input = self._csrf_token_input()
318
335
 
319
336
  buttons = []
320
337
  buttons.append(
321
338
  f'<form style="display:inline" method="post" action="{run_url}">'
322
- f'<input type="hidden" name="csrfmiddlewaretoken" value={csrf_token}>'
323
- f'<button type="submit" style="background:#17a2b8;color:#fff;padding:3px 8px;border-radius:3px;border:none;cursor:pointer;margin-right:4px;font-size:12px" title="{_("Execute now")}">'
324
- f'<i class="ri-play-line"></i> {_("Run")}</button></form>'
339
+ f'{csrf_input}'
340
+ f'<button type="submit" style="background:#17a2b8;color:#fff;{self._btn_base}" title="{_("Click to run this cronjob")}">'
341
+ f'<i class="fas fa-play"></i> {_("Run")}</button></form>'
325
342
  )
326
343
  if obj.is_active:
327
344
  btn_style = "background:#ffc107;color:#000"
328
- btn_title = _("Deactivate")
329
- btn_icon = "ri-pause-line"
345
+ btn_title = _("Click to deactivate this cronjob")
346
+ btn_icon = "fas fa-pause"
330
347
  btn_label = _("Deactivate")
331
348
  else:
332
349
  btn_style = "background:#28a745;color:#fff"
333
- btn_title = _("Activate")
334
- btn_icon = "ri-play-circle-line"
350
+ btn_title = _("Click to activate this cronjob")
351
+ btn_icon = "fas fa-play-circle"
335
352
  btn_label = _("Activate")
336
353
  buttons.append(
337
354
  f'<form style="display:inline" method="post" action="{toggle_url}">'
338
- f'<input type="hidden" name="csrfmiddlewaretoken" value={csrf_token}>'
339
- f'<button type="submit" style="{btn_style};padding:3px 8px;border-radius:3px;border:none;cursor:pointer;margin-right:4px;font-size:12px" title="{btn_title}">'
355
+ f'{csrf_input}'
356
+ f'<button type="submit" style="{btn_style};{self._btn_base}" title="{btn_title}">'
340
357
  f'<i class="{btn_icon}"></i> {btn_label}</button></form>'
341
358
  )
342
359
  delete_url = reverse('admin:django_simpletask5_cronjob_delete', args=[obj.pk])
343
360
  buttons.append(
344
- f'<a class="button" style="background:#dc3545;color:#fff;padding:3px 8px;border-radius:3px;text-decoration:none;margin-right:4px;font-size:12px" '
345
- f'href="{delete_url}" title="{_("Delete")}">'
346
- f'<i class="ri-delete-bin-line"></i> {_("Delete")}</a>'
361
+ f'<a class="button" style="background:#dc3545;color:#fff;padding:3px 8px;border-radius:3px;text-decoration:none;margin-right:4px;font-size:12px;min-width:68px;display:inline-block;text-align:center" '
362
+ f'href="{delete_url}" title="{_("Click to delete this cronjob")}">'
363
+ f'<i class="fas fa-trash"></i> {_("Delete")}</a>'
347
364
  )
348
365
  return mark_safe(''.join(buttons))
349
366
 
@@ -10,7 +10,17 @@ logger = logging.getLogger(__name__)
10
10
  _CRONJOB_REGISTRY = {}
11
11
 
12
12
 
13
- def register_cronjob(uid, display_name, cron_expression, executor_class, context=None, description='', is_active=True):
13
+ def register_cronjob(uid, display_name, cron_expression, executor_class, context=None, description='', is_active=True, app_label=''):
14
+ if not app_label:
15
+ import inspect
16
+ frame = inspect.currentframe()
17
+ try:
18
+ caller_frame = frame.f_back
19
+ caller_module = inspect.getmodule(caller_frame)
20
+ if caller_module and caller_module.__name__:
21
+ app_label = caller_module.__name__.split('.')[0]
22
+ finally:
23
+ del frame
14
24
  _CRONJOB_REGISTRY[uid] = {
15
25
  'display_name': display_name,
16
26
  'cron_expression': cron_expression,
@@ -18,6 +28,7 @@ def register_cronjob(uid, display_name, cron_expression, executor_class, context
18
28
  'context': json.dumps(context) if context else None,
19
29
  'description': description,
20
30
  'is_active': is_active,
31
+ 'app_label': app_label,
21
32
  }
22
33
 
23
34
 
@@ -56,6 +67,7 @@ def sync_cronjobs():
56
67
  cronjob.context = config.get('context')
57
68
  cronjob.description = config.get('description', '')
58
69
  cronjob.is_active = config.get('is_active', True)
70
+ cronjob.app_label = config.get('app_label', '')
59
71
  if cronjob.next_run_time is None:
60
72
  cronjob.next_run_time = _compute_next_run_time(config['cron_expression'])
61
73
  cronjob.save()
@@ -68,6 +80,7 @@ def sync_cronjobs():
68
80
  context=config.get('context'),
69
81
  description=config.get('description', ''),
70
82
  is_active=config.get('is_active', True),
83
+ app_label=config.get('app_label', ''),
71
84
  is_modified_by_user=False,
72
85
  next_run_time=_compute_next_run_time(config['cron_expression']),
73
86
  )
@@ -80,35 +93,35 @@ def register_builtin_cronjobs():
80
93
 
81
94
  register_cronjob(
82
95
  uid='simpletask_status_check',
83
- display_name=_('Status Check'),
96
+ display_name=_('Task Timeout Detection'),
84
97
  cron_expression='* * * * *',
85
98
  executor_class='django_simpletask5.executors.status_check.StatusCheckExecutor',
86
99
  description='Scan long-running pending/running tasks and mark them as timeout',
87
100
  )
88
101
  register_cronjob(
89
102
  uid='simpletask_retry_timeout',
90
- display_name=_('Retry Timeout'),
103
+ display_name=_('Retry Failed Tasks'),
91
104
  cron_expression='* * * * *',
92
105
  executor_class='django_simpletask5.executors.retry_timeout.RetryTimeoutExecutor',
93
106
  description='Retry timeout tasks that still have retry attempts remaining',
94
107
  )
95
108
  register_cronjob(
96
109
  uid='simpletask_archive',
97
- display_name=_('Archive'),
110
+ display_name=_('Execution Archive'),
98
111
  cron_expression='0 2 * * *',
99
112
  executor_class='django_simpletask5.executors.archive.ArchiveExecutor',
100
113
  description='Archive previous day TaskExecution records',
101
114
  )
102
115
  register_cronjob(
103
116
  uid='simpletask_purge_expired',
104
- display_name=_('Purge Expired'),
117
+ display_name=_('Purge Expired Records'),
105
118
  cron_expression='0 3 * * *',
106
119
  executor_class='django_simpletask5.executors.archive.PurgeExpiredExecutor',
107
120
  description='Purge expired TaskExecution records beyond retention days',
108
121
  )
109
122
  register_cronjob(
110
123
  uid='simpletask_ping_pong',
111
- display_name=_('Ping / Pong'),
124
+ display_name=_('Health Check'),
112
125
  cron_expression='* * * * *',
113
126
  executor_class='django_simpletask5.executors.ping_pong.PingPongExecutor',
114
127
  description='Ping pong health check',
@@ -1,7 +1,22 @@
1
1
  from django.conf import settings
2
2
 
3
+
4
+ def _compute_key_prefix() -> str:
5
+ custom = getattr(settings, 'DJANGO_SIMPLETASK_KEY_PREFIX', None)
6
+ if custom is not None:
7
+ return custom
8
+ root_urlconf = getattr(settings, 'ROOT_URLCONF', None)
9
+ if root_urlconf:
10
+ return root_urlconf.split('.')[0]
11
+ return 'default'
12
+
13
+
14
+ DJANGO_SIMPLETASK_KEY_PREFIX = _compute_key_prefix()
15
+
3
16
  DJANGO_SIMPLETASK_LOCK_TIMEOUT = getattr(settings, 'DJANGO_SIMPLETASK_LOCK_TIMEOUT', 600)
17
+ DJANGO_SIMPLETASK_LOCK_RENEW_INTERVAL = getattr(settings, 'DJANGO_SIMPLETASK_LOCK_RENEW_INTERVAL', 60)
4
18
 
19
+ DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT = getattr(settings, 'DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT', 30)
5
20
  DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES', 3)
6
21
  DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS', 600)
7
22
  DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS', 3600)
@@ -12,8 +27,8 @@ DJANGO_SIMPLETASK_ARCHIVE_RETENTION_DAYS = getattr(settings, 'DJANGO_SIMPLETASK_
12
27
 
13
28
  DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC = getattr(settings, 'DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC', True)
14
29
 
15
- DJANGO_SIMPLETASK_DEFAULT_QUEUE = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_QUEUE', 'django_simpletask5.queue.default')
16
- DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = getattr(settings, 'DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE', 'django_simpletask5.queue.high_priority')
30
+ DJANGO_SIMPLETASK_DEFAULT_QUEUE = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_QUEUE', f'{DJANGO_SIMPLETASK_KEY_PREFIX}:django-simpletask5:queue:default')
31
+ DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = getattr(settings, 'DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE', f'{DJANGO_SIMPLETASK_KEY_PREFIX}:django-simpletask5:queue:high_priority')
17
32
 
18
33
  DJANGO_SIMPLETASK_ENABLE_SCRIPT_EXECUTORS = getattr(settings, 'DJANGO_SIMPLETASK_ENABLE_SCRIPT_EXECUTORS', False)
19
34
  DJANGO_SIMPLETASK_SCRIPT_WHITELIST = getattr(settings, 'DJANGO_SIMPLETASK_SCRIPT_WHITELIST', [])
@@ -0,0 +1,22 @@
1
+ import logging
2
+
3
+ from django.conf import settings
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ def _default_lock_config():
9
+ return {
10
+ 'global_lock_engine_class': 'globallock.django_redis_global_lock.DjangoRedisGlobalLock',
11
+ 'global_lock_engine_options': {
12
+ 'redis-cache-name': 'default',
13
+ },
14
+ }
15
+
16
+
17
+ def get_lock_manager():
18
+ from globallock import GlobalLockManager
19
+ config = getattr(settings, 'DJANGO_SIMPLETASK_LOCK_CONFIG', None)
20
+ if config is None:
21
+ config = _default_lock_config()
22
+ return GlobalLockManager(config)
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import socket
3
+ import time
3
4
 
4
5
  from django.conf import settings
5
6
  from kombu import Connection, Exchange, Queue, Producer, Consumer
@@ -53,32 +54,29 @@ class KombuQueue:
53
54
  retry=True,
54
55
  )
55
56
 
56
- def consume(self, queue_name, callback, auto_ack=True):
57
- retry_delay = 1
58
- while not self._stopped:
59
- try:
60
- with Connection(_get_broker_url()) as conn:
61
- conn.connect()
62
- queue = Queue(queue_name, self._exchange, routing_key=queue_name, durable=True)
57
+ def consume(self, queue_name, callback, auto_ack=True, poll_callback=None, poll_interval=60):
58
+ with Connection(_get_broker_url()) as conn:
59
+ conn.connect()
60
+ queue = Queue(queue_name, self._exchange, routing_key=queue_name, durable=True)
63
61
 
64
- with Consumer(
65
- conn,
66
- queues=[queue],
67
- callbacks=[lambda body, msg: self._on_message(body, msg, callback, auto_ack)],
68
- accept=['json'],
69
- ):
70
- retry_delay = 1
71
- while not self._stopped:
72
- try:
73
- conn.drain_events(timeout=0.5)
74
- except socket.timeout:
75
- continue
76
- except Exception as e:
77
- if not self._stopped:
78
- logger.debug('Consumer connection lost: %s, reconnecting in %ss...', e, retry_delay)
79
- import time as _time
80
- _time.sleep(retry_delay)
81
- retry_delay = min(retry_delay * 2, 10)
62
+ with Consumer(
63
+ conn,
64
+ queues=[queue],
65
+ callbacks=[lambda body, msg: self._on_message(body, msg, callback, auto_ack)],
66
+ accept=['json'],
67
+ ):
68
+ last_poll = time.time()
69
+ while not self._stopped:
70
+ try:
71
+ conn.drain_events(timeout=0.5)
72
+ except socket.timeout:
73
+ pass
74
+ if poll_callback and time.time() - last_poll >= poll_interval:
75
+ try:
76
+ poll_callback()
77
+ except Exception:
78
+ logger.exception('Poll callback failed')
79
+ last_poll = time.time()
82
80
 
83
81
  @staticmethod
84
82
  def _on_message(body, message, callback, auto_ack):
@@ -33,6 +33,8 @@ def _resolve_pickup_timeout_seconds(executor_class_path: str) -> int:
33
33
 
34
34
 
35
35
  def create_and_publish(task, trigger_event, extra_context=None):
36
+ if getattr(task, '_skip_publish', False):
37
+ return None
36
38
  executor_class_path = task.get_executor_class_path(event=trigger_event)
37
39
  queue_name = task.get_queue_name(event=trigger_event)
38
40
 
@@ -63,6 +65,51 @@ def create_and_publish(task, trigger_event, extra_context=None):
63
65
  return execution
64
66
 
65
67
 
68
+ def bulk_create_and_publish(tasks, trigger_event, extra_context=None):
69
+ executions = []
70
+ for task in tasks:
71
+ if getattr(task, '_skip_publish', False):
72
+ continue
73
+ executor_class_path = task.get_executor_class_path(event=trigger_event)
74
+ queue_name = task.get_queue_name(event=trigger_event)
75
+
76
+ context = {
77
+ 'task_id': str(task.task_id),
78
+ 'trigger_event': trigger_event,
79
+ 'data': _model_to_dict(task),
80
+ }
81
+ if extra_context:
82
+ context.update(extra_context)
83
+
84
+ serialized_context = _serialize_context(context)
85
+
86
+ execution = TaskExecution(
87
+ task_id=task.task_id,
88
+ task_model=task._meta.label,
89
+ trigger_event=trigger_event,
90
+ executor_class=executor_class_path,
91
+ status='pending',
92
+ timeout_seconds=_resolve_timeout_seconds(executor_class_path),
93
+ pickup_timeout_seconds=_resolve_pickup_timeout_seconds(executor_class_path),
94
+ context=json.dumps(serialized_context, default=str) if serialized_context else None,
95
+ )
96
+ executions.append((execution, queue_name))
97
+
98
+ if not executions:
99
+ return []
100
+
101
+ TaskExecution.objects.bulk_create([e for e, _ in executions])
102
+
103
+ transaction.on_commit(lambda: _bulk_publish(executions))
104
+
105
+ return [e for e, _ in executions]
106
+
107
+
108
+ def _bulk_publish(executions):
109
+ for execution, routing_key in executions:
110
+ _publish_message(execution, routing_key=routing_key)
111
+
112
+
66
113
  def _publish_message(execution, routing_key=None):
67
114
  message = {
68
115
  'execution_id': str(execution.execution_id),
@@ -46,11 +46,13 @@ def _safe_redis_call(fn, default=None, log_level=logging.WARNING):
46
46
 
47
47
 
48
48
  def _worker_key(pid, tid):
49
- return f'simpletask:worker:{pid}.{tid}'
49
+ from django_simpletask5.core.defaults import DJANGO_SIMPLETASK_KEY_PREFIX
50
+ return f'{DJANGO_SIMPLETASK_KEY_PREFIX}:django-simpletask5:worker:{pid}:{tid}'
50
51
 
51
52
 
52
53
  def _workers_pattern():
53
- return 'simpletask:worker:*'
54
+ from django_simpletask5.core.defaults import DJANGO_SIMPLETASK_KEY_PREFIX
55
+ return f'{DJANGO_SIMPLETASK_KEY_PREFIX}:django-simpletask5:worker:*'
54
56
 
55
57
 
56
58
  def register_worker(queue_name, services=None, pid=None, tid=None):
@@ -133,4 +135,5 @@ def get_worker_stats():
133
135
  'by_queue': by_queue,
134
136
  'by_status': by_status,
135
137
  'workers': workers,
138
+ 'redis_available': not _REDIS_UNAVAILABLE,
136
139
  }