django-simpletask5 0.2.6__tar.gz → 0.2.8__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.
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/PKG-INFO +63 -14
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/README.md +61 -12
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/cronjob_registry.py +2 -2
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/defaults.py +0 -3
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/message_queue.py +38 -1
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/publisher.py +12 -4
- django_simpletask5-0.2.8/django_simpletask5/core/reclaim.py +189 -0
- django_simpletask5-0.2.8/django_simpletask5/core/security.py +31 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/signals.py +25 -4
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/dashboards.py +0 -3
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/bash_script.py +3 -5
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/python_script.py +3 -6
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/retry_timeout.py +0 -1
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/status_check.py +12 -2
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/management/commands/django_simpletask_crontab.py +19 -2
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/management/commands/django_simpletask_executor.py +70 -103
- django_simpletask5-0.2.8/django_simpletask5/management/commands/django_simpletask_queues.py +71 -0
- django_simpletask5-0.2.8/django_simpletask5/migrations/0013_alter_taskexecution_task_id.py +24 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/models.py +50 -9
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/services/archive.py +0 -1
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5.egg-info/PKG-INFO +63 -14
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5.egg-info/SOURCES.txt +4 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5.egg-info/requires.txt +1 -1
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/pyproject.toml +2 -2
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/LICENSE +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/__init__.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/admin.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/apps.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/__init__.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/executor_scanner.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/lock.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/logging_utils.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/worker_registry.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/__init__.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/archive.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/base.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/exceptions.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/loader.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/ping_pong.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/executors/simple_request.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.po +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/management/__init__.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/management/commands/__init__.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/management/commands/django_simpletask_archive.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/management/commands/django_simpletask_sync_cronjobs.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0001_initial.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0002_taskexecution_done_tasks_count_and_more.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0003_alter_cronjob_options_remove_cronjob_name_and_more.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0004_alter_cronjob_is_modified_by_user.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0005_alter_cronjob_is_modified_by_user_help.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0006_alter_cronjob_options_alter_taskexecution_created_at_and_more.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0007_rename_task_model_to_executor_class_in_stat.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0008_taskexecution_pickup_timeout_seconds.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0009_add_cronjob_app_label.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0010_taskexecution_id_alter_taskexecution_execution_id.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0011_taskexecutionstat_canceled_count.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/0012_alter_taskexecution_status.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/migrations/__init__.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/services/__init__.py +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/static/django_simpletask5/css/dashboard.css +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/static/django_simpletask5/js/select2_executor.js +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5.egg-info/dependency_links.txt +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5.egg-info/top_level.txt +0 -0
- {django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-simpletask5
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
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>
|
|
@@ -23,7 +23,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
23
23
|
Requires-Python: >=3.8
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
|
-
Requires-Dist: globallock>=0.
|
|
26
|
+
Requires-Dist: globallock>=0.2.0
|
|
27
27
|
Requires-Dist: django-safe-fields>=0.2.3
|
|
28
28
|
Requires-Dist: django-app-requires>=0.3.6
|
|
29
29
|
Requires-Dist: django-admin-dashboards>=0.1.1
|
|
@@ -143,6 +143,7 @@ DJANGO_SIMPLETASK_BROKER_URL = 'amqp://guest:guest@127.0.0.1:5672//'
|
|
|
143
143
|
# DJANGO_SIMPLETASK_REDIS_MQ_HOST = '127.0.0.1' # MESSAGE_QUEUE_TYPE='redis' 时
|
|
144
144
|
# DJANGO_SIMPLETASK_REDIS_MQ_PORT = 6379
|
|
145
145
|
# DJANGO_SIMPLETASK_REDIS_MQ_DB = 0
|
|
146
|
+
# DJANGO_SIMPLETASK_REDIS_MQ_PASSWORD = None # Redis MQ 需认证时设置
|
|
146
147
|
|
|
147
148
|
# ── 分布式锁与 Worker 注册中心(Redis)────────────────────────
|
|
148
149
|
# 默认锁引擎 DjangoRedisGlobalLock 复用 CACHES['default'],需为 Redis 缓存
|
|
@@ -153,13 +154,16 @@ CACHES = {
|
|
|
153
154
|
'OPTIONS': {'CLIENT_CLASS': 'django_redis.client.DefaultClient'},
|
|
154
155
|
},
|
|
155
156
|
}
|
|
156
|
-
# 如需独立连接参数(锁与 Worker 注册中心共用同一 host/port/db):
|
|
157
|
+
# 如需独立连接参数(锁与 Worker 注册中心共用同一 host/port/db/password):
|
|
157
158
|
# DJANGO_SIMPLETASK_LOCK_CONFIG = {
|
|
158
159
|
# 'global_lock_engine_class': 'globallock.redis_global_lock.RedisGlobalLock',
|
|
159
|
-
# 'global_lock_engine_options': {'host': '127.0.0.1', 'port': 6379, 'db': 0
|
|
160
|
+
# 'global_lock_engine_options': {'host': '127.0.0.1', 'port': 6379, 'db': 0,
|
|
161
|
+
# 'password': '<redis 密码,无需认证可省略>'},
|
|
160
162
|
# }
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
# 注:globallock>=0.2.0 已取消按 timeout 自动释放的概念。锁的有效期由
|
|
164
|
+
# lockman/lock 作用域自行管理,作用域未结束前锁持续有效(内置看门狗自动续期),
|
|
165
|
+
# 锁在 `with` 块退出或进程消亡时自动释放。已移除 DJANGO_SIMPLETASK_LOCK_TIMEOUT
|
|
166
|
+
# 与 DJANGO_SIMPLETASK_LOCK_RENEW_INTERVAL,请勿再配置这两项。
|
|
163
167
|
|
|
164
168
|
# ── Key 前缀与队列路由 ───────────────────────────────────────
|
|
165
169
|
DJANGO_SIMPLETASK_KEY_PREFIX = 'myproject' # Redis key 前缀,默认取 ROOT_URLCONF 首段
|
|
@@ -209,11 +213,9 @@ django_app_requires_patch_all()
|
|
|
209
213
|
| `DJANGO_SIMPLETASK_BROKER_URL` | `None` | **优先级最高**,设置后忽略下方 MQ 类型/连接参数。生产建议统一使用完整 URL,便于复用云厂商连接串与凭据管理 |
|
|
210
214
|
| `DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE` | `'memory'` | `memory` 仅用于开发调试;**生产必须改为 `rabbitmq` 或 `redis`**,否则消息不持久化、跨进程 Worker 无法消费 |
|
|
211
215
|
| `DJANGO_SIMPLETASK_RABBITMQ_HOST/PORT/USER/PASSWORD` | `127.0.0.1 / 5672 / guest / guest` | 类型为 `rabbitmq` 且未设 `BROKER_URL` 时生效。生产务必更换默认 `guest` 凭据 |
|
|
212
|
-
| `DJANGO_SIMPLETASK_REDIS_MQ_HOST/PORT/DB` | `127.0.0.1 / 6379 / 0` | 类型为 `redis` 时生效。建议与锁/缓存使用**不同 db**,避免 key
|
|
216
|
+
| `DJANGO_SIMPLETASK_REDIS_MQ_HOST/PORT/DB/PASSWORD` | `127.0.0.1 / 6379 / 0 / None` | 类型为 `redis` 时生效。建议与锁/缓存使用**不同 db**,避免 key 冲突与互相阻塞。Redis 需认证时设置 `PASSWORD` |
|
|
213
217
|
| `CACHES['default']` | — | 默认锁引擎 `DjangoRedisGlobalLock` 依赖它,**必须配置为 Redis 缓存** |
|
|
214
|
-
| `DJANGO_SIMPLETASK_LOCK_CONFIG` | `None`(默认复用缓存锁) |
|
|
215
|
-
| `DJANGO_SIMPLETASK_LOCK_TIMEOUT` | `600` | 锁自动释放时间,应**大于单任务最长执行时长**;过短会导致长任务执行中被其他 Worker 抢锁 |
|
|
216
|
-
| `DJANGO_SIMPLETASK_LOCK_RENEW_INTERVAL` | `60` | 锁续期间隔,应**远小于 `LOCK_TIMEOUT`**(默认 600 的 1/10) |
|
|
218
|
+
| `DJANGO_SIMPLETASK_LOCK_CONFIG` | `None`(默认复用缓存锁) | 需要独立连接参数时显式配置(`global_lock_engine_options` 支持 `host/port/db/password`)。**注意**:Worker 注册中心(心跳/状态)也读取该配置的 `host/port/db/password`,未配置时固定连 `127.0.0.1:6379/0`。globallock>=0.2.0 起锁有效期由 lockman/lock 作用域管理(内置看门狗自动续期),**不再需要配置 `LOCK_TIMEOUT` / `LOCK_RENEW_INTERVAL`** |
|
|
217
219
|
| `DJANGO_SIMPLETASK_KEY_PREFIX` | 自动取 `ROOT_URLCONF` 首段 | 生产建议**显式指定**,多项目共用 Redis 时避免 key 冲突 |
|
|
218
220
|
| `DJANGO_SIMPLETASK_DEFAULT_QUEUE` | `{prefix}:...:queue:default` | 默认队列名。多环境(dev/staging/prod)建议加环境后缀隔离 |
|
|
219
221
|
| `DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE` | `{prefix}:...:queue:high_priority` | 高优先级队列,**需单独启动 Worker 消费** |
|
|
@@ -276,7 +278,7 @@ class OrderTask(Task):
|
|
|
276
278
|
```
|
|
277
279
|
|
|
278
280
|
要点:
|
|
279
|
-
- 主键为 Django 默认自增 `id
|
|
281
|
+
- 主键为 Django 默认自增 `id`。`Task` 不再内置 `task_id` 字段,业务标识由业务模型自行定义(自定义主键或自然键均可)
|
|
280
282
|
- 保存 / 删除 / 批量创建自动触发事件,详见「事件处理机制」
|
|
281
283
|
- 事件上下文、update 字段判定、delete 拦截等行为的完整说明见对应章节
|
|
282
284
|
|
|
@@ -352,7 +354,7 @@ OrderTask.objects.create(order_id='ORD-001', customer_name='Test', amount=Decima
|
|
|
352
354
|
| `update` | 字段值发生变化后保存 | `task_id`、`trigger_event`、`data`、`old_data`、`new_data`、`changed_fields` |
|
|
353
355
|
| `delete` | 调用 `instance.delete()` | `task_id`、`trigger_event`、`data` |
|
|
354
356
|
|
|
355
|
-
执行器通过 `execution.get_context_dict()`
|
|
357
|
+
执行器通过 `execution.get_context_dict()` 获取以上上下文。其中 `task_id` 是业务对象主键的字符串表示(如 `"42"`、UUID 字符串或自定义键);业务对象反查见「模型与生命周期」。
|
|
356
358
|
|
|
357
359
|
#### update 事件的字段变更判定
|
|
358
360
|
|
|
@@ -391,6 +393,8 @@ class OrderDeleteExecutor(BaseExecutor):
|
|
|
391
393
|
|
|
392
394
|
这样实现删除操作与外部系统清理的解耦,且支持失败重试。
|
|
393
395
|
|
|
396
|
+
`confirm_delete()` 与 `execution.get_task()` 都会通过 `TaskExecution.task_id`(业务主键字符串)反查业务对象,反查逻辑由业务模型上的 `get_task_instance()` 提供(见「模型与生命周期」)。
|
|
397
|
+
|
|
394
398
|
### 事件 → 执行器映射
|
|
395
399
|
|
|
396
400
|
每个事件消息最终要由一个执行器处理。`executor_class` 声明「事件名 → 执行器类路径」的分派表,可以是字符串(所有事件共用同一个执行器)或字典(按事件分派):
|
|
@@ -566,10 +570,38 @@ class OrderCreateExecutor(BaseExecutor):
|
|
|
566
570
|
|
|
567
571
|
所有业务任务模型继承 `Task`:
|
|
568
572
|
|
|
569
|
-
-
|
|
573
|
+
- 主键为 Django 默认自增 `id`,无需自定义;**不内置** `task_id` 字段,业务标识由业务模型按需定义
|
|
570
574
|
- 提供 `trigger(event, extra_context)`、`skip_publish()`、`get_executor_class_path(event)`、`get_queue_name(event)` 等方法
|
|
571
575
|
- 保存、删除、批量创建自动触发事件(见「事件处理机制」)
|
|
572
576
|
|
|
577
|
+
#### 业务对象反查:`get_task_instance()`
|
|
578
|
+
|
|
579
|
+
`TaskExecution.task_id` 存的是业务对象主键的字符串表示(`CharField`)。需要反查业务对象时(`execution.get_task()` / `execution.confirm_delete()`),框架先通过 `task_model` 解析出模型类,再调用该类的 `get_task_instance(task_id)` 完成查询。
|
|
580
|
+
|
|
581
|
+
默认实现在 `Task` 基类中按主键 `pk` 查询,并通过内置的 `_coerce_task_pk()` 兼容常见主键类型:
|
|
582
|
+
|
|
583
|
+
- `AutoField` / `BigAutoField` / `SmallAutoField` — 字符串转为 `int`
|
|
584
|
+
- `UUIDField` — 字符串转为 `uuid.UUID`
|
|
585
|
+
- `CharField` / `TextField` 等字符串主键 — 原样使用
|
|
586
|
+
|
|
587
|
+
```python
|
|
588
|
+
task = execution.get_task() # 返回业务对象(反查不到返回 None)
|
|
589
|
+
execution.confirm_delete() # 反查并真实删除业务对象
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
业务模型使用自定义主键或非主键自然键时,**重载 `get_task_instance()`** 即可,框架会自动改走你的实现:
|
|
593
|
+
|
|
594
|
+
```python
|
|
595
|
+
class OrderTask(Task):
|
|
596
|
+
code = models.CharField(max_length=64, unique=True) # 非主键自然键
|
|
597
|
+
|
|
598
|
+
@classmethod
|
|
599
|
+
def get_task_instance(cls, task_id):
|
|
600
|
+
if task_id is None:
|
|
601
|
+
return None
|
|
602
|
+
return cls.objects.filter(code=task_id).first()
|
|
603
|
+
```
|
|
604
|
+
|
|
573
605
|
### TaskExecution 状态机
|
|
574
606
|
|
|
575
607
|
每条事件消息对应一条 `TaskExecution` 记录,状态流转如下:
|
|
@@ -686,6 +718,8 @@ python3 -m pytest django_simpletask5_example/tests
|
|
|
686
718
|
|
|
687
719
|
测试套件覆盖 Redis、DB、MQ 连接中断后的自动恢复场景,详见 `django_simpletask5_example/tests/test_resilience_recovery.py`。
|
|
688
720
|
|
|
721
|
+
**测试输出约定**:单元测试中对预期会触发的错误(如无效 UUID 校验、毒消息丢弃、消费者断连、执行器加载失败等)所输出的错误日志,必须在测试内屏蔽,避免污染测试输出。统一使用 `django_simpletask5_example.tests.suppress_expected_errors` 上下文管理器(整体禁用日志)或 `patch.object(..., 'logger')`(断言同时校验日志)进行屏蔽。
|
|
722
|
+
|
|
689
723
|
## 架构
|
|
690
724
|
|
|
691
725
|
```
|
|
@@ -698,6 +732,21 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
698
732
|
|
|
699
733
|
## Releases
|
|
700
734
|
|
|
735
|
+
### 0.2.8
|
|
736
|
+
|
|
737
|
+
- **移除 `Task` 冗余的 `task_id` 字段** — `TaskExecution.task_id` 由 UUID 改为 `CharField(max_length=255)`,直接用于存放业务对象主键字符串(`str(instance.pk)`),不再维护一份与业务主键重复的 UUID。消息上下文中的 `task_id` 字段名不变,值语义从 UUID 变为业务主键字符串(如 `"42"`)
|
|
738
|
+
- **业务对象反查下沉到模型层** — 新增 `Task.get_task_instance(task_id)` 类方法作为默认反查实现(按 `pk` 查询,内置 `_coerce_task_pk()` 兼容 `int` / `UUID` / 字符串主键);`TaskExecution.get_task()` / `confirm_delete()` 改为解析 `task_model` 后委托给该方法。业务模型使用自定义主键或自然键时重载 `get_task_instance()` 即可
|
|
739
|
+
- **新增迁移** — `django_simpletask5.0013_alter_taskexecution_task_id`(字段改 CharField)、`django_simpletask5_example.0003_remove_ordertask_task_id`
|
|
740
|
+
- **破坏性变更**: 依赖 `TaskExecution.task_id` 为 UUID 类型的外部消费方需适配为业务主键字符串;业务 `Task` 子类若曾使用内置 `task_id` 字段需移除
|
|
741
|
+
|
|
742
|
+
### 0.2.7
|
|
743
|
+
|
|
744
|
+
- **运行中任务回收机制重构** — 取消原先按 `expire_time` 将 running 直接判为超时的语义;`StatusCheckExecutor` 仅兜底回收长期未被 Worker 获取的 pending。新增 `core/reclaim`,由 crontab 每 ~5 秒基于执行锁探测运行中任务:锁已过期(执行器异常退出)即快速重投或直接置失败,降低孤儿任务滞留时间
|
|
745
|
+
- **重试不再搁浅任务** — 失败重试先落库 `retry` 再发布重试消息,发布失败则 requeue 当前消息,杜绝任务永久停在无人处理的 `retry` 状态;KombuQueue 依据 AMQP `x-death` 上限丢弃毒消息,避免无限 requeue 造成的队头阻塞与空转
|
|
746
|
+
- **消费者重连提示** — Worker 断连重连成功后输出 `Consumer connection restored` 恢复日志,便于运维确认消费通道已恢复
|
|
747
|
+
- **安全与健壮性加固** — broker 账号/密码按百分号编码、脚本白名单按路径段边界匹配、signals 快照按实例隔离,覆盖相关回归测试
|
|
748
|
+
- **新增测试约定** — 单元测试中对预期触发(如无效 UUID 校验、毒消息丢弃、消费者断连、执行器加载失败等)产生的错误日志必须屏蔽输出,统一使用 `django_simpletask5_example.tests.suppress_expected_errors` 或替换 logger,保证测试输出干净可读;本次同步清理全部此类预期错误日志
|
|
749
|
+
|
|
701
750
|
### 0.2.6
|
|
702
751
|
|
|
703
752
|
- **新增 `canceled` 状态** — 取消已发布但未执行的任务改为独立的终态 `canceled`(原复用 `failed`),Worker 消费到已取消消息时直接跳过;Admin 取消操作、归档/清理/统计(新增 `canceled_count`)、Dashboard 状态图均纳入该状态,取消与执行失败可清晰区分
|
|
@@ -714,7 +763,7 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
714
763
|
|
|
715
764
|
- **破坏性变更**: `TaskExecution` 主键由 `execution_id`(UUID)改为默认自增 `id`,`execution_id` 变为 `unique=True` 的唯一标识字段。已有数据库需要执行迁移 `0010_taskexecution_id_alter_taskexecution_execution_id`
|
|
716
765
|
- **定期存活日志** — Worker 与 Crontab 主循环在长时间无消息/无任务时按 `DJANGO_SIMPLETASK_LOG_INTERVAL`(默认 300 秒)定期输出存活日志,并统一带时间戳格式,便于运维确认进程未死循环
|
|
717
|
-
- **MQ 日志降噪** — MQ 消费/发布异常及 Redis
|
|
766
|
+
- **MQ 日志降噪** — MQ 消费/发布异常及 Redis 连接失败改为 `error`/`warning` 级别且无堆栈(默认按间隔节流),避免刷屏;提供 `--debug` 时输出完整堆栈。注:此处所指的"锁续期失败"是 0.2.4 版本引入的自定义锁续期机制,自 globallock>=0.2.0 起该机制已**移除**,改由 globallock 内置看门狗在 lockman/lock 作用域内自动续期,无需(也无从)单独记录续期失败日志
|
|
718
767
|
- **新增配置** — `DJANGO_SIMPLETASK_LOG_INTERVAL` 控制死循环存活日志与异常节流日志的输出间隔(秒)
|
|
719
768
|
|
|
720
769
|
### 0.2.3
|
|
@@ -103,6 +103,7 @@ DJANGO_SIMPLETASK_BROKER_URL = 'amqp://guest:guest@127.0.0.1:5672//'
|
|
|
103
103
|
# DJANGO_SIMPLETASK_REDIS_MQ_HOST = '127.0.0.1' # MESSAGE_QUEUE_TYPE='redis' 时
|
|
104
104
|
# DJANGO_SIMPLETASK_REDIS_MQ_PORT = 6379
|
|
105
105
|
# DJANGO_SIMPLETASK_REDIS_MQ_DB = 0
|
|
106
|
+
# DJANGO_SIMPLETASK_REDIS_MQ_PASSWORD = None # Redis MQ 需认证时设置
|
|
106
107
|
|
|
107
108
|
# ── 分布式锁与 Worker 注册中心(Redis)────────────────────────
|
|
108
109
|
# 默认锁引擎 DjangoRedisGlobalLock 复用 CACHES['default'],需为 Redis 缓存
|
|
@@ -113,13 +114,16 @@ CACHES = {
|
|
|
113
114
|
'OPTIONS': {'CLIENT_CLASS': 'django_redis.client.DefaultClient'},
|
|
114
115
|
},
|
|
115
116
|
}
|
|
116
|
-
# 如需独立连接参数(锁与 Worker 注册中心共用同一 host/port/db):
|
|
117
|
+
# 如需独立连接参数(锁与 Worker 注册中心共用同一 host/port/db/password):
|
|
117
118
|
# DJANGO_SIMPLETASK_LOCK_CONFIG = {
|
|
118
119
|
# 'global_lock_engine_class': 'globallock.redis_global_lock.RedisGlobalLock',
|
|
119
|
-
# 'global_lock_engine_options': {'host': '127.0.0.1', 'port': 6379, 'db': 0
|
|
120
|
+
# 'global_lock_engine_options': {'host': '127.0.0.1', 'port': 6379, 'db': 0,
|
|
121
|
+
# 'password': '<redis 密码,无需认证可省略>'},
|
|
120
122
|
# }
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
# 注:globallock>=0.2.0 已取消按 timeout 自动释放的概念。锁的有效期由
|
|
124
|
+
# lockman/lock 作用域自行管理,作用域未结束前锁持续有效(内置看门狗自动续期),
|
|
125
|
+
# 锁在 `with` 块退出或进程消亡时自动释放。已移除 DJANGO_SIMPLETASK_LOCK_TIMEOUT
|
|
126
|
+
# 与 DJANGO_SIMPLETASK_LOCK_RENEW_INTERVAL,请勿再配置这两项。
|
|
123
127
|
|
|
124
128
|
# ── Key 前缀与队列路由 ───────────────────────────────────────
|
|
125
129
|
DJANGO_SIMPLETASK_KEY_PREFIX = 'myproject' # Redis key 前缀,默认取 ROOT_URLCONF 首段
|
|
@@ -169,11 +173,9 @@ django_app_requires_patch_all()
|
|
|
169
173
|
| `DJANGO_SIMPLETASK_BROKER_URL` | `None` | **优先级最高**,设置后忽略下方 MQ 类型/连接参数。生产建议统一使用完整 URL,便于复用云厂商连接串与凭据管理 |
|
|
170
174
|
| `DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE` | `'memory'` | `memory` 仅用于开发调试;**生产必须改为 `rabbitmq` 或 `redis`**,否则消息不持久化、跨进程 Worker 无法消费 |
|
|
171
175
|
| `DJANGO_SIMPLETASK_RABBITMQ_HOST/PORT/USER/PASSWORD` | `127.0.0.1 / 5672 / guest / guest` | 类型为 `rabbitmq` 且未设 `BROKER_URL` 时生效。生产务必更换默认 `guest` 凭据 |
|
|
172
|
-
| `DJANGO_SIMPLETASK_REDIS_MQ_HOST/PORT/DB` | `127.0.0.1 / 6379 / 0` | 类型为 `redis` 时生效。建议与锁/缓存使用**不同 db**,避免 key
|
|
176
|
+
| `DJANGO_SIMPLETASK_REDIS_MQ_HOST/PORT/DB/PASSWORD` | `127.0.0.1 / 6379 / 0 / None` | 类型为 `redis` 时生效。建议与锁/缓存使用**不同 db**,避免 key 冲突与互相阻塞。Redis 需认证时设置 `PASSWORD` |
|
|
173
177
|
| `CACHES['default']` | — | 默认锁引擎 `DjangoRedisGlobalLock` 依赖它,**必须配置为 Redis 缓存** |
|
|
174
|
-
| `DJANGO_SIMPLETASK_LOCK_CONFIG` | `None`(默认复用缓存锁) |
|
|
175
|
-
| `DJANGO_SIMPLETASK_LOCK_TIMEOUT` | `600` | 锁自动释放时间,应**大于单任务最长执行时长**;过短会导致长任务执行中被其他 Worker 抢锁 |
|
|
176
|
-
| `DJANGO_SIMPLETASK_LOCK_RENEW_INTERVAL` | `60` | 锁续期间隔,应**远小于 `LOCK_TIMEOUT`**(默认 600 的 1/10) |
|
|
178
|
+
| `DJANGO_SIMPLETASK_LOCK_CONFIG` | `None`(默认复用缓存锁) | 需要独立连接参数时显式配置(`global_lock_engine_options` 支持 `host/port/db/password`)。**注意**:Worker 注册中心(心跳/状态)也读取该配置的 `host/port/db/password`,未配置时固定连 `127.0.0.1:6379/0`。globallock>=0.2.0 起锁有效期由 lockman/lock 作用域管理(内置看门狗自动续期),**不再需要配置 `LOCK_TIMEOUT` / `LOCK_RENEW_INTERVAL`** |
|
|
177
179
|
| `DJANGO_SIMPLETASK_KEY_PREFIX` | 自动取 `ROOT_URLCONF` 首段 | 生产建议**显式指定**,多项目共用 Redis 时避免 key 冲突 |
|
|
178
180
|
| `DJANGO_SIMPLETASK_DEFAULT_QUEUE` | `{prefix}:...:queue:default` | 默认队列名。多环境(dev/staging/prod)建议加环境后缀隔离 |
|
|
179
181
|
| `DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE` | `{prefix}:...:queue:high_priority` | 高优先级队列,**需单独启动 Worker 消费** |
|
|
@@ -236,7 +238,7 @@ class OrderTask(Task):
|
|
|
236
238
|
```
|
|
237
239
|
|
|
238
240
|
要点:
|
|
239
|
-
- 主键为 Django 默认自增 `id
|
|
241
|
+
- 主键为 Django 默认自增 `id`。`Task` 不再内置 `task_id` 字段,业务标识由业务模型自行定义(自定义主键或自然键均可)
|
|
240
242
|
- 保存 / 删除 / 批量创建自动触发事件,详见「事件处理机制」
|
|
241
243
|
- 事件上下文、update 字段判定、delete 拦截等行为的完整说明见对应章节
|
|
242
244
|
|
|
@@ -312,7 +314,7 @@ OrderTask.objects.create(order_id='ORD-001', customer_name='Test', amount=Decima
|
|
|
312
314
|
| `update` | 字段值发生变化后保存 | `task_id`、`trigger_event`、`data`、`old_data`、`new_data`、`changed_fields` |
|
|
313
315
|
| `delete` | 调用 `instance.delete()` | `task_id`、`trigger_event`、`data` |
|
|
314
316
|
|
|
315
|
-
执行器通过 `execution.get_context_dict()`
|
|
317
|
+
执行器通过 `execution.get_context_dict()` 获取以上上下文。其中 `task_id` 是业务对象主键的字符串表示(如 `"42"`、UUID 字符串或自定义键);业务对象反查见「模型与生命周期」。
|
|
316
318
|
|
|
317
319
|
#### update 事件的字段变更判定
|
|
318
320
|
|
|
@@ -351,6 +353,8 @@ class OrderDeleteExecutor(BaseExecutor):
|
|
|
351
353
|
|
|
352
354
|
这样实现删除操作与外部系统清理的解耦,且支持失败重试。
|
|
353
355
|
|
|
356
|
+
`confirm_delete()` 与 `execution.get_task()` 都会通过 `TaskExecution.task_id`(业务主键字符串)反查业务对象,反查逻辑由业务模型上的 `get_task_instance()` 提供(见「模型与生命周期」)。
|
|
357
|
+
|
|
354
358
|
### 事件 → 执行器映射
|
|
355
359
|
|
|
356
360
|
每个事件消息最终要由一个执行器处理。`executor_class` 声明「事件名 → 执行器类路径」的分派表,可以是字符串(所有事件共用同一个执行器)或字典(按事件分派):
|
|
@@ -526,10 +530,38 @@ class OrderCreateExecutor(BaseExecutor):
|
|
|
526
530
|
|
|
527
531
|
所有业务任务模型继承 `Task`:
|
|
528
532
|
|
|
529
|
-
-
|
|
533
|
+
- 主键为 Django 默认自增 `id`,无需自定义;**不内置** `task_id` 字段,业务标识由业务模型按需定义
|
|
530
534
|
- 提供 `trigger(event, extra_context)`、`skip_publish()`、`get_executor_class_path(event)`、`get_queue_name(event)` 等方法
|
|
531
535
|
- 保存、删除、批量创建自动触发事件(见「事件处理机制」)
|
|
532
536
|
|
|
537
|
+
#### 业务对象反查:`get_task_instance()`
|
|
538
|
+
|
|
539
|
+
`TaskExecution.task_id` 存的是业务对象主键的字符串表示(`CharField`)。需要反查业务对象时(`execution.get_task()` / `execution.confirm_delete()`),框架先通过 `task_model` 解析出模型类,再调用该类的 `get_task_instance(task_id)` 完成查询。
|
|
540
|
+
|
|
541
|
+
默认实现在 `Task` 基类中按主键 `pk` 查询,并通过内置的 `_coerce_task_pk()` 兼容常见主键类型:
|
|
542
|
+
|
|
543
|
+
- `AutoField` / `BigAutoField` / `SmallAutoField` — 字符串转为 `int`
|
|
544
|
+
- `UUIDField` — 字符串转为 `uuid.UUID`
|
|
545
|
+
- `CharField` / `TextField` 等字符串主键 — 原样使用
|
|
546
|
+
|
|
547
|
+
```python
|
|
548
|
+
task = execution.get_task() # 返回业务对象(反查不到返回 None)
|
|
549
|
+
execution.confirm_delete() # 反查并真实删除业务对象
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
业务模型使用自定义主键或非主键自然键时,**重载 `get_task_instance()`** 即可,框架会自动改走你的实现:
|
|
553
|
+
|
|
554
|
+
```python
|
|
555
|
+
class OrderTask(Task):
|
|
556
|
+
code = models.CharField(max_length=64, unique=True) # 非主键自然键
|
|
557
|
+
|
|
558
|
+
@classmethod
|
|
559
|
+
def get_task_instance(cls, task_id):
|
|
560
|
+
if task_id is None:
|
|
561
|
+
return None
|
|
562
|
+
return cls.objects.filter(code=task_id).first()
|
|
563
|
+
```
|
|
564
|
+
|
|
533
565
|
### TaskExecution 状态机
|
|
534
566
|
|
|
535
567
|
每条事件消息对应一条 `TaskExecution` 记录,状态流转如下:
|
|
@@ -646,6 +678,8 @@ python3 -m pytest django_simpletask5_example/tests
|
|
|
646
678
|
|
|
647
679
|
测试套件覆盖 Redis、DB、MQ 连接中断后的自动恢复场景,详见 `django_simpletask5_example/tests/test_resilience_recovery.py`。
|
|
648
680
|
|
|
681
|
+
**测试输出约定**:单元测试中对预期会触发的错误(如无效 UUID 校验、毒消息丢弃、消费者断连、执行器加载失败等)所输出的错误日志,必须在测试内屏蔽,避免污染测试输出。统一使用 `django_simpletask5_example.tests.suppress_expected_errors` 上下文管理器(整体禁用日志)或 `patch.object(..., 'logger')`(断言同时校验日志)进行屏蔽。
|
|
682
|
+
|
|
649
683
|
## 架构
|
|
650
684
|
|
|
651
685
|
```
|
|
@@ -658,6 +692,21 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
658
692
|
|
|
659
693
|
## Releases
|
|
660
694
|
|
|
695
|
+
### 0.2.8
|
|
696
|
+
|
|
697
|
+
- **移除 `Task` 冗余的 `task_id` 字段** — `TaskExecution.task_id` 由 UUID 改为 `CharField(max_length=255)`,直接用于存放业务对象主键字符串(`str(instance.pk)`),不再维护一份与业务主键重复的 UUID。消息上下文中的 `task_id` 字段名不变,值语义从 UUID 变为业务主键字符串(如 `"42"`)
|
|
698
|
+
- **业务对象反查下沉到模型层** — 新增 `Task.get_task_instance(task_id)` 类方法作为默认反查实现(按 `pk` 查询,内置 `_coerce_task_pk()` 兼容 `int` / `UUID` / 字符串主键);`TaskExecution.get_task()` / `confirm_delete()` 改为解析 `task_model` 后委托给该方法。业务模型使用自定义主键或自然键时重载 `get_task_instance()` 即可
|
|
699
|
+
- **新增迁移** — `django_simpletask5.0013_alter_taskexecution_task_id`(字段改 CharField)、`django_simpletask5_example.0003_remove_ordertask_task_id`
|
|
700
|
+
- **破坏性变更**: 依赖 `TaskExecution.task_id` 为 UUID 类型的外部消费方需适配为业务主键字符串;业务 `Task` 子类若曾使用内置 `task_id` 字段需移除
|
|
701
|
+
|
|
702
|
+
### 0.2.7
|
|
703
|
+
|
|
704
|
+
- **运行中任务回收机制重构** — 取消原先按 `expire_time` 将 running 直接判为超时的语义;`StatusCheckExecutor` 仅兜底回收长期未被 Worker 获取的 pending。新增 `core/reclaim`,由 crontab 每 ~5 秒基于执行锁探测运行中任务:锁已过期(执行器异常退出)即快速重投或直接置失败,降低孤儿任务滞留时间
|
|
705
|
+
- **重试不再搁浅任务** — 失败重试先落库 `retry` 再发布重试消息,发布失败则 requeue 当前消息,杜绝任务永久停在无人处理的 `retry` 状态;KombuQueue 依据 AMQP `x-death` 上限丢弃毒消息,避免无限 requeue 造成的队头阻塞与空转
|
|
706
|
+
- **消费者重连提示** — Worker 断连重连成功后输出 `Consumer connection restored` 恢复日志,便于运维确认消费通道已恢复
|
|
707
|
+
- **安全与健壮性加固** — broker 账号/密码按百分号编码、脚本白名单按路径段边界匹配、signals 快照按实例隔离,覆盖相关回归测试
|
|
708
|
+
- **新增测试约定** — 单元测试中对预期触发(如无效 UUID 校验、毒消息丢弃、消费者断连、执行器加载失败等)产生的错误日志必须屏蔽输出,统一使用 `django_simpletask5_example.tests.suppress_expected_errors` 或替换 logger,保证测试输出干净可读;本次同步清理全部此类预期错误日志
|
|
709
|
+
|
|
661
710
|
### 0.2.6
|
|
662
711
|
|
|
663
712
|
- **新增 `canceled` 状态** — 取消已发布但未执行的任务改为独立的终态 `canceled`(原复用 `failed`),Worker 消费到已取消消息时直接跳过;Admin 取消操作、归档/清理/统计(新增 `canceled_count`)、Dashboard 状态图均纳入该状态,取消与执行失败可清晰区分
|
|
@@ -674,7 +723,7 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
674
723
|
|
|
675
724
|
- **破坏性变更**: `TaskExecution` 主键由 `execution_id`(UUID)改为默认自增 `id`,`execution_id` 变为 `unique=True` 的唯一标识字段。已有数据库需要执行迁移 `0010_taskexecution_id_alter_taskexecution_execution_id`
|
|
676
725
|
- **定期存活日志** — Worker 与 Crontab 主循环在长时间无消息/无任务时按 `DJANGO_SIMPLETASK_LOG_INTERVAL`(默认 300 秒)定期输出存活日志,并统一带时间戳格式,便于运维确认进程未死循环
|
|
677
|
-
- **MQ 日志降噪** — MQ 消费/发布异常及 Redis
|
|
726
|
+
- **MQ 日志降噪** — MQ 消费/发布异常及 Redis 连接失败改为 `error`/`warning` 级别且无堆栈(默认按间隔节流),避免刷屏;提供 `--debug` 时输出完整堆栈。注:此处所指的"锁续期失败"是 0.2.4 版本引入的自定义锁续期机制,自 globallock>=0.2.0 起该机制已**移除**,改由 globallock 内置看门狗在 lockman/lock 作用域内自动续期,无需(也无从)单独记录续期失败日志
|
|
678
727
|
- **新增配置** — `DJANGO_SIMPLETASK_LOG_INTERVAL` 控制死循环存活日志与异常节流日志的输出间隔(秒)
|
|
679
728
|
|
|
680
729
|
### 0.2.3
|
{django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/cronjob_registry.py
RENAMED
|
@@ -94,10 +94,10 @@ def register_builtin_cronjobs():
|
|
|
94
94
|
|
|
95
95
|
register_cronjob(
|
|
96
96
|
uid='simpletask_status_check',
|
|
97
|
-
display_name=_('
|
|
97
|
+
display_name=_('Pickup Timeout Detection'),
|
|
98
98
|
cron_expression='* * * * *',
|
|
99
99
|
executor_class='django_simpletask5.executors.status_check.StatusCheckExecutor',
|
|
100
|
-
description='Scan
|
|
100
|
+
description='Scan pending tasks that were never picked up and mark them as timeout for retry. Running tasks are not time-limited: orphans are reclaimed by the scheduler based on execution-lock expiry',
|
|
101
101
|
)
|
|
102
102
|
register_cronjob(
|
|
103
103
|
uid='simpletask_retry_timeout',
|
|
@@ -13,9 +13,6 @@ def _compute_key_prefix() -> str:
|
|
|
13
13
|
|
|
14
14
|
DJANGO_SIMPLETASK_KEY_PREFIX = _compute_key_prefix()
|
|
15
15
|
|
|
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)
|
|
18
|
-
|
|
19
16
|
DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT = getattr(settings, 'DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT', 30)
|
|
20
17
|
DJANGO_SIMPLETASK_LOG_INTERVAL = getattr(settings, 'DJANGO_SIMPLETASK_LOG_INTERVAL', 300)
|
|
21
18
|
DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL = getattr(settings, 'DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL', 60)
|
{django_simpletask5-0.2.6 → django_simpletask5-0.2.8}/django_simpletask5/core/message_queue.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import socket
|
|
3
3
|
import time
|
|
4
|
+
from urllib.parse import quote
|
|
4
5
|
|
|
5
6
|
from django.conf import settings
|
|
6
7
|
from kombu import Connection, Exchange, Queue, Producer, Consumer
|
|
@@ -10,6 +11,11 @@ from django_simpletask5.core.defaults import DJANGO_SIMPLETASK_LOG_INTERVAL
|
|
|
10
11
|
logger = logging.getLogger(__name__)
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def _quote_url_component(value):
|
|
15
|
+
"""URL 编码账号/密码,避免密码含 @ : / % 等保留字符破坏连接串解析。"""
|
|
16
|
+
return quote(str(value), safe='')
|
|
17
|
+
|
|
18
|
+
|
|
13
19
|
def _get_broker_url():
|
|
14
20
|
url = getattr(settings, 'DJANGO_SIMPLETASK_BROKER_URL', None)
|
|
15
21
|
if url:
|
|
@@ -20,12 +26,16 @@ def _get_broker_url():
|
|
|
20
26
|
port = getattr(settings, 'DJANGO_SIMPLETASK_RABBITMQ_PORT', 5672)
|
|
21
27
|
user = getattr(settings, 'DJANGO_SIMPLETASK_RABBITMQ_USER', 'guest')
|
|
22
28
|
pw = getattr(settings, 'DJANGO_SIMPLETASK_RABBITMQ_PASSWORD', 'guest')
|
|
29
|
+
user = _quote_url_component(user)
|
|
30
|
+
pw = _quote_url_component(pw)
|
|
23
31
|
return f'amqp://{user}:{pw}@{host}:{port}//'
|
|
24
32
|
if transport == 'redis':
|
|
25
33
|
host = getattr(settings, 'DJANGO_SIMPLETASK_REDIS_MQ_HOST', '127.0.0.1')
|
|
26
34
|
port = getattr(settings, 'DJANGO_SIMPLETASK_REDIS_MQ_PORT', 6379)
|
|
27
35
|
db = getattr(settings, 'DJANGO_SIMPLETASK_REDIS_MQ_DB', 0)
|
|
28
|
-
|
|
36
|
+
pw = getattr(settings, 'DJANGO_SIMPLETASK_REDIS_MQ_PASSWORD', None)
|
|
37
|
+
auth = f':{_quote_url_component(pw)}@' if pw else ''
|
|
38
|
+
return f'redis://{auth}{host}:{port}/{db}'
|
|
29
39
|
return 'memory://'
|
|
30
40
|
|
|
31
41
|
|
|
@@ -89,15 +99,42 @@ class KombuQueue:
|
|
|
89
99
|
try:
|
|
90
100
|
result = callback(body)
|
|
91
101
|
if result is False:
|
|
102
|
+
if KombuQueue._exceeds_max_redeliveries(message):
|
|
103
|
+
logger.error(
|
|
104
|
+
'Dropping message after too many redeliveries (possible poison message): %s',
|
|
105
|
+
body,
|
|
106
|
+
)
|
|
107
|
+
message.ack()
|
|
108
|
+
return
|
|
92
109
|
message.reject(requeue=True)
|
|
93
110
|
return
|
|
94
111
|
except Exception as e:
|
|
95
112
|
logger.error('Error in message callback: %s', e)
|
|
113
|
+
if KombuQueue._exceeds_max_redeliveries(message):
|
|
114
|
+
logger.error('Dropping message after too many redeliveries (possible poison message): %s', body)
|
|
115
|
+
message.ack()
|
|
116
|
+
return
|
|
96
117
|
message.reject(requeue=True)
|
|
97
118
|
return
|
|
98
119
|
if auto_ack:
|
|
99
120
|
message.ack()
|
|
100
121
|
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _exceeds_max_redeliveries(message):
|
|
124
|
+
"""判断一条消息是否已被反复重投过多次(AMQP x-death 计数)。
|
|
125
|
+
|
|
126
|
+
防止“无法服务/暂时性失败”的消息被无限 requeue 造成队头阻塞与 CPU 空转。
|
|
127
|
+
非 AMQP 传输(memory / redis)不提供 x-death 头,返回 False 保持原行为。
|
|
128
|
+
"""
|
|
129
|
+
limit = getattr(settings, 'DJANGO_SIMPLETASK_MAX_REQUEUE_DELIVERIES', 100)
|
|
130
|
+
headers = getattr(message, 'headers', None) or {}
|
|
131
|
+
x_death = headers.get('x-death') or []
|
|
132
|
+
total = 0
|
|
133
|
+
for entry in x_death:
|
|
134
|
+
if isinstance(entry, dict):
|
|
135
|
+
total += int(entry.get('count', 0) or 0)
|
|
136
|
+
return total >= limit
|
|
137
|
+
|
|
101
138
|
def stop(self):
|
|
102
139
|
self._stopped = True
|
|
103
140
|
|
|
@@ -39,7 +39,7 @@ def create_and_publish(task, trigger_event, extra_context=None):
|
|
|
39
39
|
queue_name = task.get_queue_name(event=trigger_event)
|
|
40
40
|
|
|
41
41
|
context = {
|
|
42
|
-
'task_id':
|
|
42
|
+
'task_id': _task_identity(task),
|
|
43
43
|
'trigger_event': trigger_event,
|
|
44
44
|
'data': _model_to_dict(task),
|
|
45
45
|
}
|
|
@@ -49,7 +49,7 @@ def create_and_publish(task, trigger_event, extra_context=None):
|
|
|
49
49
|
serialized_context = _serialize_context(context)
|
|
50
50
|
|
|
51
51
|
execution = TaskExecution(
|
|
52
|
-
task_id=task
|
|
52
|
+
task_id=_task_identity(task),
|
|
53
53
|
task_model=task._meta.label,
|
|
54
54
|
trigger_event=trigger_event,
|
|
55
55
|
executor_class=executor_class_path,
|
|
@@ -74,7 +74,7 @@ def bulk_create_and_publish(tasks, trigger_event, extra_context=None):
|
|
|
74
74
|
queue_name = task.get_queue_name(event=trigger_event)
|
|
75
75
|
|
|
76
76
|
context = {
|
|
77
|
-
'task_id':
|
|
77
|
+
'task_id': _task_identity(task),
|
|
78
78
|
'trigger_event': trigger_event,
|
|
79
79
|
'data': _model_to_dict(task),
|
|
80
80
|
}
|
|
@@ -84,7 +84,7 @@ def bulk_create_and_publish(tasks, trigger_event, extra_context=None):
|
|
|
84
84
|
serialized_context = _serialize_context(context)
|
|
85
85
|
|
|
86
86
|
execution = TaskExecution(
|
|
87
|
-
task_id=task
|
|
87
|
+
task_id=_task_identity(task),
|
|
88
88
|
task_model=task._meta.label,
|
|
89
89
|
trigger_event=trigger_event,
|
|
90
90
|
executor_class=executor_class_path,
|
|
@@ -165,6 +165,14 @@ def _serialize_value(value):
|
|
|
165
165
|
return str(value)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def _task_identity(instance):
|
|
169
|
+
"""返回业务对象唯一标识字符串,用作 TaskExecution.task_id。
|
|
170
|
+
|
|
171
|
+
业务实例唯一性由其真实主键承载,而非额外的 task_id UUID。
|
|
172
|
+
"""
|
|
173
|
+
return str(instance.pk) if instance.pk is not None else None
|
|
174
|
+
|
|
175
|
+
|
|
168
176
|
def _model_to_dict(instance):
|
|
169
177
|
data = {}
|
|
170
178
|
for field in instance._meta.fields:
|