django-simpletask5 0.2.8__tar.gz → 0.2.9__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 (84) hide show
  1. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/PKG-INFO +109 -47
  2. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/README.md +106 -43
  3. django_simpletask5-0.2.9/django_simpletask5/__init__.py +13 -0
  4. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/admin.py +150 -24
  5. django_simpletask5-0.2.9/django_simpletask5/apps.py +25 -0
  6. django_simpletask5-0.2.9/django_simpletask5/core/__init__.py +1 -0
  7. django_simpletask5-0.2.9/django_simpletask5/core/cronjob_registry.py +207 -0
  8. django_simpletask5-0.2.9/django_simpletask5/core/defaults.py +86 -0
  9. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/core/executor_scanner.py +47 -0
  10. django_simpletask5-0.2.9/django_simpletask5/core/lock.py +43 -0
  11. django_simpletask5-0.2.9/django_simpletask5/core/logging_utils.py +106 -0
  12. django_simpletask5-0.2.9/django_simpletask5/core/message_queue.py +269 -0
  13. django_simpletask5-0.2.9/django_simpletask5/core/publisher.py +372 -0
  14. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/core/reclaim.py +52 -5
  15. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/core/security.py +10 -0
  16. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/core/signals.py +71 -4
  17. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/core/worker_registry.py +80 -7
  18. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/dashboards.py +22 -2
  19. django_simpletask5-0.2.9/django_simpletask5/executors/__init__.py +1 -0
  20. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/executors/archive.py +15 -0
  21. django_simpletask5-0.2.9/django_simpletask5/executors/base.py +63 -0
  22. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/executors/bash_script.py +22 -0
  23. django_simpletask5-0.2.9/django_simpletask5/executors/exceptions.py +14 -0
  24. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/executors/loader.py +10 -0
  25. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/executors/ping_pong.py +5 -0
  26. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/executors/python_script.py +18 -0
  27. django_simpletask5-0.2.9/django_simpletask5/executors/retry_timeout.py +106 -0
  28. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/executors/simple_request.py +17 -1
  29. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/executors/status_check.py +8 -0
  30. django_simpletask5-0.2.9/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  31. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.po +13 -0
  32. django_simpletask5-0.2.9/django_simpletask5/management/__init__.py +1 -0
  33. django_simpletask5-0.2.9/django_simpletask5/management/commands/__init__.py +1 -0
  34. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/management/commands/django_simpletask_archive.py +16 -0
  35. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/management/commands/django_simpletask_crontab.py +69 -7
  36. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/management/commands/django_simpletask_executor.py +130 -14
  37. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/management/commands/django_simpletask_queues.py +22 -1
  38. django_simpletask5-0.2.9/django_simpletask5/management/commands/django_simpletask_sync_cronjobs.py +24 -0
  39. django_simpletask5-0.2.9/django_simpletask5/migrations/0014_cronjob_is_active_modified_by_user.py +22 -0
  40. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/models.py +195 -7
  41. django_simpletask5-0.2.9/django_simpletask5/services/__init__.py +1 -0
  42. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/services/archive.py +62 -0
  43. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5.egg-info/PKG-INFO +109 -47
  44. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5.egg-info/SOURCES.txt +1 -0
  45. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5.egg-info/requires.txt +1 -0
  46. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/pyproject.toml +3 -4
  47. django_simpletask5-0.2.8/django_simpletask5/__init__.py +0 -10
  48. django_simpletask5-0.2.8/django_simpletask5/apps.py +0 -13
  49. django_simpletask5-0.2.8/django_simpletask5/core/cronjob_registry.py +0 -130
  50. django_simpletask5-0.2.8/django_simpletask5/core/defaults.py +0 -39
  51. django_simpletask5-0.2.8/django_simpletask5/core/lock.py +0 -22
  52. django_simpletask5-0.2.8/django_simpletask5/core/logging_utils.py +0 -17
  53. django_simpletask5-0.2.8/django_simpletask5/core/message_queue.py +0 -143
  54. django_simpletask5-0.2.8/django_simpletask5/core/publisher.py +0 -192
  55. django_simpletask5-0.2.8/django_simpletask5/executors/__init__.py +0 -0
  56. django_simpletask5-0.2.8/django_simpletask5/executors/base.py +0 -35
  57. django_simpletask5-0.2.8/django_simpletask5/executors/exceptions.py +0 -13
  58. django_simpletask5-0.2.8/django_simpletask5/executors/retry_timeout.py +0 -65
  59. django_simpletask5-0.2.8/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  60. django_simpletask5-0.2.8/django_simpletask5/management/__init__.py +0 -0
  61. django_simpletask5-0.2.8/django_simpletask5/management/commands/__init__.py +0 -0
  62. django_simpletask5-0.2.8/django_simpletask5/management/commands/django_simpletask_sync_cronjobs.py +0 -14
  63. django_simpletask5-0.2.8/django_simpletask5/migrations/__init__.py +0 -0
  64. django_simpletask5-0.2.8/django_simpletask5/services/__init__.py +0 -0
  65. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/LICENSE +0 -0
  66. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0001_initial.py +0 -0
  67. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0002_taskexecution_done_tasks_count_and_more.py +0 -0
  68. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0003_alter_cronjob_options_remove_cronjob_name_and_more.py +0 -0
  69. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0004_alter_cronjob_is_modified_by_user.py +0 -0
  70. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0005_alter_cronjob_is_modified_by_user_help.py +0 -0
  71. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0006_alter_cronjob_options_alter_taskexecution_created_at_and_more.py +0 -0
  72. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0007_rename_task_model_to_executor_class_in_stat.py +0 -0
  73. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0008_taskexecution_pickup_timeout_seconds.py +0 -0
  74. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0009_add_cronjob_app_label.py +0 -0
  75. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0010_taskexecution_id_alter_taskexecution_execution_id.py +0 -0
  76. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0011_taskexecutionstat_canceled_count.py +0 -0
  77. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0012_alter_taskexecution_status.py +0 -0
  78. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/migrations/0013_alter_taskexecution_task_id.py +0 -0
  79. {django_simpletask5-0.2.8/django_simpletask5/core → django_simpletask5-0.2.9/django_simpletask5/migrations}/__init__.py +0 -0
  80. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/static/django_simpletask5/css/dashboard.css +0 -0
  81. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5/static/django_simpletask5/js/select2_executor.js +0 -0
  82. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5.egg-info/dependency_links.txt +0 -0
  83. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/django_simpletask5.egg-info/top_level.txt +0 -0
  84. {django_simpletask5-0.2.8 → django_simpletask5-0.2.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-simpletask5
3
- Version: 0.2.8
3
+ Version: 0.2.9
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>
@@ -15,14 +15,13 @@ Classifier: Framework :: Django :: 5.1
15
15
  Classifier: Framework :: Django :: 5.2
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.8
19
- Classifier: Programming Language :: Python :: 3.9
20
18
  Classifier: Programming Language :: Python :: 3.10
21
19
  Classifier: Programming Language :: Python :: 3.11
22
20
  Classifier: Programming Language :: Python :: 3.12
23
- Requires-Python: >=3.8
21
+ Requires-Python: >=3.10
24
22
  Description-Content-Type: text/markdown
25
23
  License-File: LICENSE
24
+ Requires-Dist: Django>=4.2
26
25
  Requires-Dist: globallock>=0.2.0
27
26
  Requires-Dist: django-safe-fields>=0.2.3
28
27
  Requires-Dist: django-app-requires>=0.3.6
@@ -51,7 +50,7 @@ Dynamic: license-file
51
50
  - **自定义事件** — 通过 `task.trigger('event_name')` 触发任意事件
52
51
  - **灵活的执行器映射** — 不同事件可绑定不同的执行器类
53
52
  - **队列路由** — 不同事件可路由到不同优先级队列
54
- - **重试与超时** — 失败自动重试(指数退避),支持执行超时与 Pickup 超时两种检测
53
+ - **重试与超时** — 失败自动重试(指数退避),支持 Pickup 超时检测;运行中孤儿任务由执行锁回收
55
54
  - **拒绝回退** — 执行器可通过 `RejectException` 拒绝当前任务,消息自动重新入队等待下次调度
56
55
  - **加密字段** — 敏感数据自动加密存储
57
56
  - **Cron 调度** — 内置 crontab 守护进程,支持代码注册与数据库覆盖
@@ -61,10 +60,10 @@ Dynamic: license-file
61
60
 
62
61
  ## 依赖
63
62
 
64
- - Python >= 3.8
65
- - Django >= 3.2(兼容至 5.2.x)
66
- - Kombu >= 5.3.0(消息队列,支持 RabbitMQ/Redis/内存)
67
- - Redis >= 4.0.0(分布式锁、Worker 注册中心)
63
+ - Python >= 3.10
64
+ - Django >= 4.2(兼容至 5.2.x)
65
+ - Kombu >= 5.6.2(消息队列,支持 RabbitMQ/Redis/内存)
66
+ - Redis >= 7.4.0(分布式锁、Worker 注册中心)
68
67
  - 详见 `pyproject.toml`
69
68
 
70
69
  ## 安装
@@ -134,6 +133,8 @@ INSTALLED_APPS = [
134
133
  # ── 消息队列(MQ)────────────────────────────────────────────
135
134
  # 生产使用 RabbitMQ 或 Redis;开发调试可用内存队列
136
135
  DJANGO_SIMPLETASK_BROKER_URL = 'amqp://guest:guest@127.0.0.1:5672//'
136
+ DJANGO_SIMPLETASK_BROKER_HEARTBEAT = 60 # 发布连接池的 broker 心跳(秒)
137
+ DJANGO_SIMPLETASK_MAX_REQUEUE_DELIVERIES = 100 # AMQP 消息最大重投次数,超过后丢弃
137
138
  # 若不设 BROKER_URL,则按以下参数组合:
138
139
  # DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE = 'rabbitmq' # 'memory' | 'rabbitmq' | 'redis'
139
140
  # DJANGO_SIMPLETASK_RABBITMQ_HOST = '127.0.0.1'
@@ -172,16 +173,28 @@ DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = 'myproject:django-simpletask5:queue:high
172
173
 
173
174
  # ── 执行与重试 ───────────────────────────────────────────────
174
175
  DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES = 3
175
- DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS = 600 # 执行超时(秒)
176
+ DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS = 600 # 执行超时字段(秒,当前不主动中断)
176
177
  DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS = 3600 # Pickup 超时(秒)
177
178
 
179
+ # ── 发布重试 ────────────────────────────────────────────────
180
+ DJANGO_SIMPLETASK_PUBLISH_RETRY_POLICY = {
181
+ 'max_retries': 2,
182
+ 'interval_start': 0.2,
183
+ 'interval_step': 0.3,
184
+ 'interval_max': 1.0,
185
+ }
186
+ DJANGO_SIMPLETASK_PUBLISH_RETRY_BASE_DELAY = 1.0 # 应用层第 1 次重试等待(秒)
187
+ DJANGO_SIMPLETASK_PUBLISH_RETRY_MAX_DELAY = 5.0 # 应用层重试等待上限(秒)
188
+
178
189
  # ── Worker / 日志运维参数 ────────────────────────────────────
179
- DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL = 60 # Worker 心跳间隔(秒)
180
- DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT = 30 # 优雅停机等待(秒)
181
- DJANGO_SIMPLETASK_LOG_INTERVAL = 300 # 存活/节流日志间隔(秒)
190
+ DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL = 60 # Worker 心跳/巡检间隔(秒)
191
+ DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT = 30 # 每个 worker 线程的优雅停机等待(秒)
192
+ DJANGO_SIMPLETASK_LOG_INTERVAL = 300 # 存活/注册失败等日志节流间隔(秒)
193
+ DJANGO_SIMPLETASK_PUBLISH_LOG_INTERVAL = 60 # 发布/回收失败日志节流间隔(秒)
182
194
 
183
195
  # ── 归档与统计 ──────────────────────────────────────────────
184
- DJANGO_SIMPLETASK_ARCHIVE_PATH = os.path.join(BASE_DIR, 'django_simpletask5_archives')
196
+ # 归档文件通过 TaskExecutionArchive.file 写入 MEDIA_ROOT/django_simpletask5_archives/%Y/%m/
197
+ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
185
198
  DJANGO_SIMPLETASK_ARCHIVE_SALT = 'change-me-archive-salt' # 归档加密盐值
186
199
  DJANGO_SIMPLETASK_ARCHIVE_RETENTION_DAYS = 7
187
200
 
@@ -214,18 +227,24 @@ django_app_requires_patch_all()
214
227
  | `DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE` | `'memory'` | `memory` 仅用于开发调试;**生产必须改为 `rabbitmq` 或 `redis`**,否则消息不持久化、跨进程 Worker 无法消费 |
215
228
  | `DJANGO_SIMPLETASK_RABBITMQ_HOST/PORT/USER/PASSWORD` | `127.0.0.1 / 5672 / guest / guest` | 类型为 `rabbitmq` 且未设 `BROKER_URL` 时生效。生产务必更换默认 `guest` 凭据 |
216
229
  | `DJANGO_SIMPLETASK_REDIS_MQ_HOST/PORT/DB/PASSWORD` | `127.0.0.1 / 6379 / 0 / None` | 类型为 `redis` 时生效。建议与锁/缓存使用**不同 db**,避免 key 冲突与互相阻塞。Redis 需认证时设置 `PASSWORD` |
230
+ | `DJANGO_SIMPLETASK_BROKER_HEARTBEAT` | `60` | Kombu 发布连接池的 broker heartbeat(秒)。网络中间设备会断开空闲连接时建议保持开启 |
231
+ | `DJANGO_SIMPLETASK_MAX_REQUEUE_DELIVERIES` | `100` | AMQP 消息被 reject/requeue 的最大次数,超过后 ack 丢弃,防止毒消息无限循环;memory/redis 传输无 `x-death` 时不生效 |
217
232
  | `CACHES['default']` | — | 默认锁引擎 `DjangoRedisGlobalLock` 依赖它,**必须配置为 Redis 缓存** |
218
233
  | `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`** |
219
234
  | `DJANGO_SIMPLETASK_KEY_PREFIX` | 自动取 `ROOT_URLCONF` 首段 | 生产建议**显式指定**,多项目共用 Redis 时避免 key 冲突 |
220
235
  | `DJANGO_SIMPLETASK_DEFAULT_QUEUE` | `{prefix}:...:queue:default` | 默认队列名。多环境(dev/staging/prod)建议加环境后缀隔离 |
221
236
  | `DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE` | `{prefix}:...:queue:high_priority` | 高优先级队列,**需单独启动 Worker 消费** |
222
237
  | `DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES` | `3` | 默认最大重试次数。执行器幂等性好的任务可加大 |
223
- | `DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS` | `600` | 执行超时,应**大于任务实际最长执行时间**,否则任务被误判超时 |
238
+ | `DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS` | `600` | 写入 `TaskExecution.timeout_seconds`,并用于计算 running 时的 `expire_time`。当前版本不会因此主动中断 running 任务;严格超时应由执行器自行实现 |
224
239
  | `DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS` | `3600` | Pickup 超时(等待 Worker 消费的上限),**通常大于执行超时**,因为排队等待时间不可控 |
225
- | `DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL` | `60` | Worker 心跳间隔,应**小于 Worker 注册 TTL(300 秒)**,否则注册状态被误清理 |
226
- | `DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT` | `30` | 优雅停机等待时长,应**大于单任务最长执行时长**,否则停机会中断进行中的任务 |
227
- | `DJANGO_SIMPLETASK_LOG_INTERVAL` | `300` | 存活日志与异常节流日志的输出间隔。日志量大时可调大 |
228
- | `DJANGO_SIMPLETASK_ARCHIVE_PATH` | `django_simpletask5_archives` | 归档目录。生产建议指向**持久化/对象存储**挂载目录 |
240
+ | `DJANGO_SIMPLETASK_PUBLISH_RETRY_POLICY` | `{'max_retries': 2, 'interval_start': 0.2, 'interval_step': 0.3, 'interval_max': 1.0}` | Kombu Producer 内层重试策略。应用层还会做最多 3 次发布尝试 |
241
+ | `DJANGO_SIMPLETASK_PUBLISH_RETRY_BASE_DELAY` | `1.0` | 应用层发布重试第 1 次等待(秒),之后按指数退避 |
242
+ | `DJANGO_SIMPLETASK_PUBLISH_RETRY_MAX_DELAY` | `5.0` | 应用层单次发布重试等待上限(秒) |
243
+ | `DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL` | `60` | Worker 心跳/巡检间隔,应**小于 Worker 注册 TTL(300 秒)**,否则注册状态被误清理 |
244
+ | `DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT` | `30` | **每个** worker 线程的优雅停机等待时长;总等待时间可能为 worker 数乘以该值 |
245
+ | `DJANGO_SIMPLETASK_LOG_INTERVAL` | `300` | Worker 注册失败、Crontab 错误等日志的节流间隔。日志量大时可调大 |
246
+ | `DJANGO_SIMPLETASK_PUBLISH_LOG_INTERVAL` | `60` | 发布、重投、孤儿回收失败日志的节流间隔(秒) |
247
+ | `DJANGO_SIMPLETASK_ARCHIVE_PATH` | `django_simpletask5_archives` | **当前版本未读取,仅保留兼容**。归档文件实际写入 `MEDIA_ROOT/django_simpletask5_archives/%Y/%m/` |
229
248
  | `DJANGO_SIMPLETASK_ARCHIVE_SALT` | `django-simpletask5-archive` | 归档加密盐值,**生产必须修改**(结合 `SECRET_KEY` 派生 AES 密钥) |
230
249
  | `DJANGO_SIMPLETASK_ARCHIVE_RETENTION_DAYS` | `7` | 归档保留天数,按合规要求调整 |
231
250
  | `DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC` | `True` | 自动把代码注册的 Cron 同步到数据库;只想手工管理时设为 `False` |
@@ -238,6 +257,8 @@ django_app_requires_patch_all()
238
257
 
239
258
  > 注意:`DJANGO_SIMPLETASK_ENABLE_SCRIPT_EXECUTORS` 设为 `True` 后,还需为用户/组授予 `django_simpletask5 | Cron job | Can use script executors` 权限,非超级管理员无法创建 Python/Shell 脚本执行器类型的定时任务。
240
259
 
260
+ > 发布可靠性说明:应用层 `_publish_message` 最多尝试 3 次(首次 + 2 次重试),失败等待按 `PUBLISH_RETRY_BASE_DELAY` 指数增长并受 `PUBLISH_RETRY_MAX_DELAY` 限制;Kombu Producer 内部还会按 `PUBLISH_RETRY_POLICY` 重试。最终仍失败时,`safe_publish_message()` 会把非终态 execution 标记为 `failed`,不会让调用方因 broker 异常崩溃。`publish_message()` 保留异常上抛语义,供需要自行处理错误的调用方使用。
261
+
241
262
  ## 快速开始(从零到一集成)
242
263
 
243
264
  本框架的集成分三步:**定义任务模型 → 编写执行器 → 启动 Worker**。完整可运行的参考实现见仓库中的 `django_simpletask5_example` 应用,包含 `OrderTask` 模型、创建/更新/删除/退款四个执行器及内置 Cron 任务。
@@ -292,7 +313,7 @@ from django_simpletask5.executors.base import BaseExecutor
292
313
  from django_simpletask5.models import TaskExecution
293
314
 
294
315
  class OrderCreateExecutor(BaseExecutor):
295
- timeout_seconds = 300 # 执行超时 5 分钟(可选)
316
+ timeout_seconds = 300 # 记录预期执行上限(当前不主动中断,可选)
296
317
  pickup_timeout_seconds = 1800 # Pickup 超时 30 分钟(可选)
297
318
 
298
319
  def execute(self, execution: TaskExecution, task=None) -> str | None:
@@ -486,7 +507,7 @@ OrderTask.objects.create(..., skip_publish=True) # 方式四:构造参数(
486
507
  | `get_tasks(execution)` | 返回可迭代对象,默认 `yield None`(单任务) |
487
508
  | `get_tasks_count(execution)` | 任务总数(用于进度展示),默认 `1` |
488
509
  | `reject(execution, reason='')` | 拒绝任务:状态复位 `pending`、清空 `started_at`、刷新 `expire_time` |
489
- | `timeout_seconds` / `pickup_timeout_seconds` | 类属性,覆盖该执行器的超时配置 |
510
+ | `timeout_seconds` / `pickup_timeout_seconds` | 类属性。`pickup_timeout_seconds` 用于自动超时判定;`timeout_seconds` 当前仅记录预期执行上限,不主动中断任务 |
490
511
  | `get_parameter_schema()` | 声明执行器参数,用于 Admin 参数配置界面(默认空) |
491
512
 
492
513
  ### execute 签名与返回值
@@ -558,12 +579,14 @@ class MyExecutor(BaseExecutor):
558
579
 
559
580
  ```python
560
581
  class OrderCreateExecutor(BaseExecutor):
561
- timeout_seconds = 300 # 执行超时 5 分钟
582
+ timeout_seconds = 300 # 记录预期执行上限(当前不主动中断)
562
583
  pickup_timeout_seconds = 1800 # Pickup 超时 30 分钟
563
584
  ```
564
585
 
565
586
  未配置时使用全局默认值 `DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS`(600 秒)与 `DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS`(3600 秒)。
566
587
 
588
+ > 注意:当前版本只有 `pickup_timeout_seconds` 会被框架自动判定并触发 `timeout`;`timeout_seconds` 不会中止正在运行的执行器。需要严格执行时限时,请在 `execute()` 内部自行实现超时或使用可取消的子进程。
589
+
567
590
  ## 模型与生命周期
568
591
 
569
592
  ### Task 抽象模型
@@ -608,21 +631,23 @@ class OrderTask(Task):
608
631
 
609
632
  ```
610
633
  pending ──(Worker 消费)──▶ running ──(执行成功)──▶ success
611
- │
612
- ├─(执行异常且重试次数用完)──▶ failed
613
- ├─(执行异常但可重试)────────▶ retry ──(重新入队)──▶ pending
614
- └─(expire_time 超时)────────▶ timeout ──(重试)────▶ retry
615
- pending ──(用户取消)────────▶ canceled(终态)
634
+ │ │
635
+ │ ├─(异常且可重试)────────▶ retry ──(重新入队并被消费)──▶ running
636
+ │ ├─(异常且重试耗尽)──────▶ failed
637
+ │ └─(执行器异常退出/锁丢失)─▶ retry 或 failed(由 reclaim 回收)
638
+ ├─(pickup 超时,从未被领取)──▶ timeout ──(RetryTimeout)──▶ retry
639
+ ├─(用户取消)────────────────▶ canceled(终态)
640
+ └─(发布最终失败)────────────▶ failed
616
641
  ```
617
642
 
618
643
  | 状态 | 含义 |
619
644
  |---|---|
620
- | `pending` | 已发布,等待 Worker 消费 |
621
- | `running` | 正在执行 |
645
+ | `pending` | 已发布,等待 Worker 消费;拒绝回退也会回到该状态 |
646
+ | `running` | 已获取执行锁,正在执行 |
622
647
  | `success` | 执行成功 |
623
- | `failed` | 重试次数耗尽后失败 |
624
- | `retry` | 失败但可重试,消息已重新入队 |
625
- | `timeout` | 超过 `expire_time` 未完成 |
648
+ | `failed` | 重试次数耗尽、发布最终失败,或孤儿回收时已无重试次数 |
649
+ | `retry` | 失败/超时后等待重新投递,消息已重新入队 |
650
+ | `timeout` | **pending** 超过 pickup 期限仍未被领取;running 不会因 `expire_time` 自动变成 timeout |
626
651
  | `canceled` | 被用户主动取消(终态,Worker 不会执行) |
627
652
 
628
653
  ### 取消任务
@@ -642,29 +667,29 @@ TaskExecution.objects.filter(
642
667
  ### 重试与超时
643
668
 
644
669
  - **重试**:`max_retries`(默认 3)控制最大重试次数,`retry_count` 累计已重试次数;重试消息重新发布到原队列
645
- - **两种超时**:
646
- - **执行超时**(`timeout_seconds`):进入 `running` 后必须在指定时间内完成,防止执行 hang 死
647
- - **Pickup 超时**(`pickup_timeout_seconds`):创建后被 Worker 消费的等待上限,也用于拒绝回退后的重新调度保护
648
- - **超时检测与恢复**:内置 `StatusCheckExecutor`(内置 Cron)将超时的 `pending`/`running` 记录标记为 `timeout`;`RetryTimeoutExecutor` 将 `timeout` 记录重新入队重试(执行超时消耗重试次数,Pickup 超时不消耗)
670
+ - **Pickup 超时**(`pickup_timeout_seconds`):**唯一由框架自动判定的超时**。`pending` 超过 `expire_time` 后由 `StatusCheckExecutor` 标记为 `timeout`,也用于拒绝回退后的重新调度保护;Pickup 超时不消耗重试次数
671
+ - **执行超时字段**(`timeout_seconds`):写入 `TaskExecution.timeout_seconds`,并在任务进入 `running` 时用于计算 `expire_time`。当前版本**不会**因该时间到达而主动中断 running 任务;如果执行器 hang 住,需在执行器内部实现超时/取消机制
672
+ - **超时检测与恢复**:`StatusCheckExecutor` 每分钟把超过 pickup 期限的 `pending` 记录标记为 `timeout`;`RetryTimeoutExecutor` 每分钟把 `timeout` 记录重新入队,实际运行过的记录消耗一次重试次数。running 孤儿任务由 crontab 基于执行锁过期回收,而不是按 `expire_time` 判超时
649
673
 
650
674
  ### 其他模型
651
675
 
652
676
  - `CronJob` — 定时任务注册表,支持代码注册与数据库覆盖(见「Cron 任务」)
653
- - `TaskExecutionArchive` — 归档记录,已完成执行归档为加密 JSONL(见配置「归档」)
654
- - `TaskExecutionStat` — 按 `executor_class` 与日期聚合的日统计
677
+ - `TaskExecutionArchive` — 归档记录,终态执行记录归档为加密 JSONL(见配置「归档」)
678
+ - `TaskExecutionStat` — 按日期、`executor_class`、`trigger_event` 聚合的日统计
655
679
 
656
680
  ## Cron 任务
657
681
 
658
682
  ### 在代码中定义 Cron 任务
659
683
 
660
- 在任意 app 的 `cronjobs.py` 文件中使用 `register_cronjob` 注册:
684
+ 在任意 app 的 `cronjobs.py` 文件中使用 `register_cronjob` 注册,并在该 app 的 `AppConfig.ready()` 中导入这个模块;框架不会自动发现各 app 下的 `cronjobs.py`:
661
685
 
662
686
  ```python
663
687
  # myapp/cronjobs.py
664
- from django_simpletask5.cronjob_registry import register_cronjob
688
+ from django_simpletask5.core.cronjob_registry import register_cronjob
665
689
 
666
690
  register_cronjob(
667
- name='health_check',
691
+ uid='health_check',
692
+ display_name='Health Check',
668
693
  cron_expression='*/5 * * * *',
669
694
  executor_class='django_simpletask5.executors.simple_request.SimpleRequestExecutor',
670
695
  context={
@@ -676,15 +701,33 @@ register_cronjob(
676
701
  )
677
702
  ```
678
703
 
704
+ ```python
705
+ # myapp/apps.py
706
+ from django.apps import AppConfig
707
+
708
+ class MyAppConfig(AppConfig):
709
+ name = 'myapp'
710
+
711
+ def ready(self):
712
+ import myapp.cronjobs # noqa: F401
713
+ ```
714
+
679
715
  ### 从代码同步到数据库
680
716
 
681
- 注册的 Cron 任务需要同步到数据库才会生效。框架默认在 `django_simpletask_crontab` 启动时自动同步(可通过 `DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC = False` 关闭),也可以手动执行:
717
+ 注册的 Cron 任务需要同步到数据库才会生效。`django_simpletask_crontab` 运行期间每个 tick 都会自动同步(默认开启,可通过 `DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC = False` 关闭自动同步),也可以手动执行:
682
718
 
683
719
  ```bash
684
720
  python manage.py django_simpletask_sync_cronjobs
685
721
  ```
686
722
 
687
- 同步后,用户可以在 Django Admin 中查看和修改 Cron 任务,被手动修改过的任务不会在后续同步中被覆盖(`is_modified_by_user` 标记保护)。
723
+ 手动同步命令使用 `sync_cronjobs(force=True)`,即使关闭了自动同步也会执行。同步后,用户可以在 Django Admin 中查看和修改 Cron 任务,被手动修改过的任务不会在后续同步中被覆盖(`is_modified_by_user` 标记保护)。单个 CronJob 创建失败只记录日志,不会中断其它任务同步。
724
+
725
+ 启用/停用(`is_active`)的所有权独立跟踪,规则与 `is_modified_by_user` 对称——**手动改过就一直归你,直到显式重置**:
726
+
727
+ - 用户**未**手动启停过的任务,`is_active` 会按代码注册值同步(例如 `simpletask_ping_pong` 注册为 `is_active=False`,默认保持停用)。
728
+ - 用户手动暂停/启用后,会置 `is_active_modified_by_user` 标记,自动同步不再覆盖该状态,但**不影响**其它定义字段(`cron_expression` 等仍照常同步)。
729
+ - 该标记不会自动清除;需要让它重新跟随代码时,使用管理后台的“从代码定义重置”(`reset_from_code`),或在编辑页取消勾选“保留手动启用状态”。
730
+ - 这样手动启停的状态可稳定保持,不会因代码默认值变化而悄悄被改写。
688
731
 
689
732
  > 注意:cron 表达式按 Django 的 `TIME_ZONE` 设置对应的本地时间解释。`USE_TZ=True` 时仍以本地时间计算触发时刻(存储为 UTC),例如 `TIME_ZONE = 'Asia/Shanghai'` 下 `30 14 * * *` 表示每天 14:30 本地时间触发。
690
733
 
@@ -692,13 +735,14 @@ python manage.py django_simpletask_sync_cronjobs
692
735
 
693
736
  | 执行器 | 说明 |
694
737
  |---|---|
695
- | `PingPongExecutor` | 健康检查,返回 `'pong'` |
738
+ | `PingPongExecutor` | 健康检查,返回 `'pong'`;内置 Cron 默认停用 |
696
739
  | `BashScriptExecutor` | 执行 Shell 脚本 |
697
740
  | `PythonScriptExecutor` | 执行 Python 代码 |
698
741
  | `SimpleRequestExecutor` | 发起 HTTP 请求 |
699
- | `StatusCheckExecutor` | 检测卡住的执行并标记超时(每 5 分钟) |
700
- | `RetryTimeoutExecutor` | 重试超时的执行(每 10 分钟) |
701
- | `ArchiveExecutor` | 归档已完成执行并生成统计(每天凌晨 2 点) |
742
+ | `StatusCheckExecutor` | 检测超过 pickup 期限、从未被领取的 `pending` 记录并标记 `timeout`(内置 Cron 每分钟) |
743
+ | `RetryTimeoutExecutor` | 将 `timeout` 记录重新入队(内置 Cron 每分钟;运行过的记录消耗一次重试) |
744
+ | `ArchiveExecutor` | 归档前一天的终态执行并生成统计(内置 Cron 每天 2:00) |
745
+ | `PurgeExpiredExecutor` | 删除超过 `ARCHIVE_RETENTION_DAYS` 的终态执行记录(内置 Cron 每天 3:00;不删除归档文件) |
702
746
 
703
747
  ## 开发与测试
704
748
 
@@ -732,6 +776,24 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
732
776
 
733
777
  ## Releases
734
778
 
779
+ ### 0.2.9
780
+
781
+ - **启用状态所有权独立跟踪** — 新增 `CronJob.is_active_modified_by_user` 字段与迁移 `0014`,将“启用/暂停状态”与“定义字段”的所有权彻底分离。用户手动启停(快捷按钮 / 批量操作 / 编辑表单)后置该标记,自动同步(`sync_cronjobs`)不再覆盖 `is_active`;未手动改过的任务仍按代码注册值同步。暂停只锁 `is_active`,`cron_expression` 等定义字段照常同步
782
+ - **显式释放所有权** — 该标记不会自动清除;需要让任务重新跟随代码时,使用管理后台“从代码定义重置”(`reset_from_code`),或在编辑页取消勾选“保留手动启用状态”
783
+ - **保留列表分页/过滤条件** — 修复暂停/运行按钮提交后重定向丢失查询串的问题:按钮表单 `action` 及视图重定向均携带当前 changelist 的 `GET` 参数,操作后刷新仍停留在原分页、过滤器与搜索条件
784
+ - **消息发布可靠性增强** — Kombu 发布复用连接池并支持 broker heartbeat;`publish()` 返回 `queue` / `message_count` / `consumer_count` 队列统计;新增可配置发布重试策略和递增重试延迟;发布异常时自动丢弃损坏连接;新增公开的 `publish_message()` 与兜底的 `safe_publish_message()`,最终发布失败会将 execution 标记为 `failed`
785
+ - **发布日志节流** — 发布、重试投递、孤儿任务回收和 CronJob 触发失败统一接入日志节流,避免 broker/Redis 长时间不可用时刷屏;重投日志同时记录队列积压与消费者数量
786
+ - **Worker 与回收健壮性** — Worker 注册表 Redis 客户端改为公开 `get_redis_client()`;修正 Worker 清理 key 的 glob 为 `worker:{pid}:*`;`reclaim._lock_config()` 与 `get_lock_manager()` 对齐,只有 `None` 才回退默认锁配置;回收扫描前后关闭旧数据库连接,减少长时间运行后的连接失效
787
+ - **日志初始化修复** — `setup_console_logging()` 不再把 `FileHandler` 误判为控制台 handler,避免已有文件日志时跳过控制台输出
788
+ - **CronJob 同步健壮性** — `sync_cronjobs(force=True)` 支持手动同步命令绕过 `DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC`;单条 CronJob 创建失败不再中断其它任务同步;`django_simpletask_sync_cronjobs` 命令改为强制同步
789
+ - **API 命名整理** — `_CRONJOB_REGISTRY` → `CRONJOB_REGISTRY`、`_compute_next_run_time()` → `compute_next_run_time()`、`_get_broker_url()` → `get_broker_url()`、`_model_to_dict()` → `model_to_dict()`、`_DeleteIntercepted` → `DeleteIntercepted`、`_get_redis_client()` → `get_redis_client()`;依赖旧私有名称的扩展或测试需同步调整
790
+ - **可观测性入口公开** — Dashboard 使用的 Worker 统计入口由 `_get_worker_stats` 改为公开的 `get_worker_stats()`;消费者线程心跳改为读取 `DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL`,Worker 注册、Redis 降级和日志节流逻辑补充了完整说明
791
+ - **注释与文档校准** — 为整个 `django_simpletask5` 生产包补充中文模块/类/函数/分支注释,并修正归档路径、归档清理、`running/timeout` 状态流转、Admin 只读性、脚本白名单、连接池 heartbeat、优雅退出等待时间等与实际行为不一致的说明
792
+ - **迁移与翻译** — 新增迁移 `django_simpletask5.0014_cronjob_is_active_modified_by_user`,同步更新简体中文翻译与迁移中的字段 `help_text`
793
+ - **测试** — 新增 `test_logging_utils.py`;扩充消息发布、消息队列、CronJob 同步、Admin 启停、Reclaim、Worker 恢复、Redis 锁与集成测试,覆盖最终发布失败、队列积压统计、`force` 同步、启停所有权、查询串保留、空锁配置和 Worker 清理 glob 等场景
794
+ - **环境要求** — 代码使用 `X | None` 联合类型,最低 Python 版本明确为 3.10;同时将 Django 最低版本明确为 4.2,并补充 `Django>=4.2` 依赖声明
795
+ - **版本** — `pyproject.toml` 版本更新为 `0.2.9`
796
+
735
797
  ### 0.2.8
736
798
 
737
799
  - **移除 `Task` 冗余的 `task_id` 字段** — `TaskExecution.task_id` 由 UUID 改为 `CharField(max_length=255)`,直接用于存放业务对象主键字符串(`str(instance.pk)`),不再维护一份与业务主键重复的 UUID。消息上下文中的 `task_id` 字段名不变,值语义从 UUID 变为业务主键字符串(如 `"42"`)
@@ -11,7 +11,7 @@
11
11
  - **自定义事件** — 通过 `task.trigger('event_name')` 触发任意事件
12
12
  - **灵活的执行器映射** — 不同事件可绑定不同的执行器类
13
13
  - **队列路由** — 不同事件可路由到不同优先级队列
14
- - **重试与超时** — 失败自动重试(指数退避),支持执行超时与 Pickup 超时两种检测
14
+ - **重试与超时** — 失败自动重试(指数退避),支持 Pickup 超时检测;运行中孤儿任务由执行锁回收
15
15
  - **拒绝回退** — 执行器可通过 `RejectException` 拒绝当前任务,消息自动重新入队等待下次调度
16
16
  - **加密字段** — 敏感数据自动加密存储
17
17
  - **Cron 调度** — 内置 crontab 守护进程,支持代码注册与数据库覆盖
@@ -21,10 +21,10 @@
21
21
 
22
22
  ## 依赖
23
23
 
24
- - Python >= 3.8
25
- - Django >= 3.2(兼容至 5.2.x)
26
- - Kombu >= 5.3.0(消息队列,支持 RabbitMQ/Redis/内存)
27
- - Redis >= 4.0.0(分布式锁、Worker 注册中心)
24
+ - Python >= 3.10
25
+ - Django >= 4.2(兼容至 5.2.x)
26
+ - Kombu >= 5.6.2(消息队列,支持 RabbitMQ/Redis/内存)
27
+ - Redis >= 7.4.0(分布式锁、Worker 注册中心)
28
28
  - 详见 `pyproject.toml`
29
29
 
30
30
  ## 安装
@@ -94,6 +94,8 @@ INSTALLED_APPS = [
94
94
  # ── 消息队列(MQ)────────────────────────────────────────────
95
95
  # 生产使用 RabbitMQ 或 Redis;开发调试可用内存队列
96
96
  DJANGO_SIMPLETASK_BROKER_URL = 'amqp://guest:guest@127.0.0.1:5672//'
97
+ DJANGO_SIMPLETASK_BROKER_HEARTBEAT = 60 # 发布连接池的 broker 心跳(秒)
98
+ DJANGO_SIMPLETASK_MAX_REQUEUE_DELIVERIES = 100 # AMQP 消息最大重投次数,超过后丢弃
97
99
  # 若不设 BROKER_URL,则按以下参数组合:
98
100
  # DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE = 'rabbitmq' # 'memory' | 'rabbitmq' | 'redis'
99
101
  # DJANGO_SIMPLETASK_RABBITMQ_HOST = '127.0.0.1'
@@ -132,16 +134,28 @@ DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE = 'myproject:django-simpletask5:queue:high
132
134
 
133
135
  # ── 执行与重试 ───────────────────────────────────────────────
134
136
  DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES = 3
135
- DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS = 600 # 执行超时(秒)
137
+ DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS = 600 # 执行超时字段(秒,当前不主动中断)
136
138
  DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS = 3600 # Pickup 超时(秒)
137
139
 
140
+ # ── 发布重试 ────────────────────────────────────────────────
141
+ DJANGO_SIMPLETASK_PUBLISH_RETRY_POLICY = {
142
+ 'max_retries': 2,
143
+ 'interval_start': 0.2,
144
+ 'interval_step': 0.3,
145
+ 'interval_max': 1.0,
146
+ }
147
+ DJANGO_SIMPLETASK_PUBLISH_RETRY_BASE_DELAY = 1.0 # 应用层第 1 次重试等待(秒)
148
+ DJANGO_SIMPLETASK_PUBLISH_RETRY_MAX_DELAY = 5.0 # 应用层重试等待上限(秒)
149
+
138
150
  # ── Worker / 日志运维参数 ────────────────────────────────────
139
- DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL = 60 # Worker 心跳间隔(秒)
140
- DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT = 30 # 优雅停机等待(秒)
141
- DJANGO_SIMPLETASK_LOG_INTERVAL = 300 # 存活/节流日志间隔(秒)
151
+ DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL = 60 # Worker 心跳/巡检间隔(秒)
152
+ DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT = 30 # 每个 worker 线程的优雅停机等待(秒)
153
+ DJANGO_SIMPLETASK_LOG_INTERVAL = 300 # 存活/注册失败等日志节流间隔(秒)
154
+ DJANGO_SIMPLETASK_PUBLISH_LOG_INTERVAL = 60 # 发布/回收失败日志节流间隔(秒)
142
155
 
143
156
  # ── 归档与统计 ──────────────────────────────────────────────
144
- DJANGO_SIMPLETASK_ARCHIVE_PATH = os.path.join(BASE_DIR, 'django_simpletask5_archives')
157
+ # 归档文件通过 TaskExecutionArchive.file 写入 MEDIA_ROOT/django_simpletask5_archives/%Y/%m/
158
+ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
145
159
  DJANGO_SIMPLETASK_ARCHIVE_SALT = 'change-me-archive-salt' # 归档加密盐值
146
160
  DJANGO_SIMPLETASK_ARCHIVE_RETENTION_DAYS = 7
147
161
 
@@ -174,18 +188,24 @@ django_app_requires_patch_all()
174
188
  | `DJANGO_SIMPLETASK_MESSAGE_QUEUE_TYPE` | `'memory'` | `memory` 仅用于开发调试;**生产必须改为 `rabbitmq` 或 `redis`**,否则消息不持久化、跨进程 Worker 无法消费 |
175
189
  | `DJANGO_SIMPLETASK_RABBITMQ_HOST/PORT/USER/PASSWORD` | `127.0.0.1 / 5672 / guest / guest` | 类型为 `rabbitmq` 且未设 `BROKER_URL` 时生效。生产务必更换默认 `guest` 凭据 |
176
190
  | `DJANGO_SIMPLETASK_REDIS_MQ_HOST/PORT/DB/PASSWORD` | `127.0.0.1 / 6379 / 0 / None` | 类型为 `redis` 时生效。建议与锁/缓存使用**不同 db**,避免 key 冲突与互相阻塞。Redis 需认证时设置 `PASSWORD` |
191
+ | `DJANGO_SIMPLETASK_BROKER_HEARTBEAT` | `60` | Kombu 发布连接池的 broker heartbeat(秒)。网络中间设备会断开空闲连接时建议保持开启 |
192
+ | `DJANGO_SIMPLETASK_MAX_REQUEUE_DELIVERIES` | `100` | AMQP 消息被 reject/requeue 的最大次数,超过后 ack 丢弃,防止毒消息无限循环;memory/redis 传输无 `x-death` 时不生效 |
177
193
  | `CACHES['default']` | — | 默认锁引擎 `DjangoRedisGlobalLock` 依赖它,**必须配置为 Redis 缓存** |
178
194
  | `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`** |
179
195
  | `DJANGO_SIMPLETASK_KEY_PREFIX` | 自动取 `ROOT_URLCONF` 首段 | 生产建议**显式指定**,多项目共用 Redis 时避免 key 冲突 |
180
196
  | `DJANGO_SIMPLETASK_DEFAULT_QUEUE` | `{prefix}:...:queue:default` | 默认队列名。多环境(dev/staging/prod)建议加环境后缀隔离 |
181
197
  | `DJANGO_SIMPLETASK_HIGH_PRIORITY_QUEUE` | `{prefix}:...:queue:high_priority` | 高优先级队列,**需单独启动 Worker 消费** |
182
198
  | `DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES` | `3` | 默认最大重试次数。执行器幂等性好的任务可加大 |
183
- | `DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS` | `600` | 执行超时,应**大于任务实际最长执行时间**,否则任务被误判超时 |
199
+ | `DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS` | `600` | 写入 `TaskExecution.timeout_seconds`,并用于计算 running 时的 `expire_time`。当前版本不会因此主动中断 running 任务;严格超时应由执行器自行实现 |
184
200
  | `DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS` | `3600` | Pickup 超时(等待 Worker 消费的上限),**通常大于执行超时**,因为排队等待时间不可控 |
185
- | `DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL` | `60` | Worker 心跳间隔,应**小于 Worker 注册 TTL(300 秒)**,否则注册状态被误清理 |
186
- | `DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT` | `30` | 优雅停机等待时长,应**大于单任务最长执行时长**,否则停机会中断进行中的任务 |
187
- | `DJANGO_SIMPLETASK_LOG_INTERVAL` | `300` | 存活日志与异常节流日志的输出间隔。日志量大时可调大 |
188
- | `DJANGO_SIMPLETASK_ARCHIVE_PATH` | `django_simpletask5_archives` | 归档目录。生产建议指向**持久化/对象存储**挂载目录 |
201
+ | `DJANGO_SIMPLETASK_PUBLISH_RETRY_POLICY` | `{'max_retries': 2, 'interval_start': 0.2, 'interval_step': 0.3, 'interval_max': 1.0}` | Kombu Producer 内层重试策略。应用层还会做最多 3 次发布尝试 |
202
+ | `DJANGO_SIMPLETASK_PUBLISH_RETRY_BASE_DELAY` | `1.0` | 应用层发布重试第 1 次等待(秒),之后按指数退避 |
203
+ | `DJANGO_SIMPLETASK_PUBLISH_RETRY_MAX_DELAY` | `5.0` | 应用层单次发布重试等待上限(秒) |
204
+ | `DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL` | `60` | Worker 心跳/巡检间隔,应**小于 Worker 注册 TTL(300 秒)**,否则注册状态被误清理 |
205
+ | `DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT` | `30` | **每个** worker 线程的优雅停机等待时长;总等待时间可能为 worker 数乘以该值 |
206
+ | `DJANGO_SIMPLETASK_LOG_INTERVAL` | `300` | Worker 注册失败、Crontab 错误等日志的节流间隔。日志量大时可调大 |
207
+ | `DJANGO_SIMPLETASK_PUBLISH_LOG_INTERVAL` | `60` | 发布、重投、孤儿回收失败日志的节流间隔(秒) |
208
+ | `DJANGO_SIMPLETASK_ARCHIVE_PATH` | `django_simpletask5_archives` | **当前版本未读取,仅保留兼容**。归档文件实际写入 `MEDIA_ROOT/django_simpletask5_archives/%Y/%m/` |
189
209
  | `DJANGO_SIMPLETASK_ARCHIVE_SALT` | `django-simpletask5-archive` | 归档加密盐值,**生产必须修改**(结合 `SECRET_KEY` 派生 AES 密钥) |
190
210
  | `DJANGO_SIMPLETASK_ARCHIVE_RETENTION_DAYS` | `7` | 归档保留天数,按合规要求调整 |
191
211
  | `DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC` | `True` | 自动把代码注册的 Cron 同步到数据库;只想手工管理时设为 `False` |
@@ -198,6 +218,8 @@ django_app_requires_patch_all()
198
218
 
199
219
  > 注意:`DJANGO_SIMPLETASK_ENABLE_SCRIPT_EXECUTORS` 设为 `True` 后,还需为用户/组授予 `django_simpletask5 | Cron job | Can use script executors` 权限,非超级管理员无法创建 Python/Shell 脚本执行器类型的定时任务。
200
220
 
221
+ > 发布可靠性说明:应用层 `_publish_message` 最多尝试 3 次(首次 + 2 次重试),失败等待按 `PUBLISH_RETRY_BASE_DELAY` 指数增长并受 `PUBLISH_RETRY_MAX_DELAY` 限制;Kombu Producer 内部还会按 `PUBLISH_RETRY_POLICY` 重试。最终仍失败时,`safe_publish_message()` 会把非终态 execution 标记为 `failed`,不会让调用方因 broker 异常崩溃。`publish_message()` 保留异常上抛语义,供需要自行处理错误的调用方使用。
222
+
201
223
  ## 快速开始(从零到一集成)
202
224
 
203
225
  本框架的集成分三步:**定义任务模型 → 编写执行器 → 启动 Worker**。完整可运行的参考实现见仓库中的 `django_simpletask5_example` 应用,包含 `OrderTask` 模型、创建/更新/删除/退款四个执行器及内置 Cron 任务。
@@ -252,7 +274,7 @@ from django_simpletask5.executors.base import BaseExecutor
252
274
  from django_simpletask5.models import TaskExecution
253
275
 
254
276
  class OrderCreateExecutor(BaseExecutor):
255
- timeout_seconds = 300 # 执行超时 5 分钟(可选)
277
+ timeout_seconds = 300 # 记录预期执行上限(当前不主动中断,可选)
256
278
  pickup_timeout_seconds = 1800 # Pickup 超时 30 分钟(可选)
257
279
 
258
280
  def execute(self, execution: TaskExecution, task=None) -> str | None:
@@ -446,7 +468,7 @@ OrderTask.objects.create(..., skip_publish=True) # 方式四:构造参数(
446
468
  | `get_tasks(execution)` | 返回可迭代对象,默认 `yield None`(单任务) |
447
469
  | `get_tasks_count(execution)` | 任务总数(用于进度展示),默认 `1` |
448
470
  | `reject(execution, reason='')` | 拒绝任务:状态复位 `pending`、清空 `started_at`、刷新 `expire_time` |
449
- | `timeout_seconds` / `pickup_timeout_seconds` | 类属性,覆盖该执行器的超时配置 |
471
+ | `timeout_seconds` / `pickup_timeout_seconds` | 类属性。`pickup_timeout_seconds` 用于自动超时判定;`timeout_seconds` 当前仅记录预期执行上限,不主动中断任务 |
450
472
  | `get_parameter_schema()` | 声明执行器参数,用于 Admin 参数配置界面(默认空) |
451
473
 
452
474
  ### execute 签名与返回值
@@ -518,12 +540,14 @@ class MyExecutor(BaseExecutor):
518
540
 
519
541
  ```python
520
542
  class OrderCreateExecutor(BaseExecutor):
521
- timeout_seconds = 300 # 执行超时 5 分钟
543
+ timeout_seconds = 300 # 记录预期执行上限(当前不主动中断)
522
544
  pickup_timeout_seconds = 1800 # Pickup 超时 30 分钟
523
545
  ```
524
546
 
525
547
  未配置时使用全局默认值 `DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS`(600 秒)与 `DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS`(3600 秒)。
526
548
 
549
+ > 注意:当前版本只有 `pickup_timeout_seconds` 会被框架自动判定并触发 `timeout`;`timeout_seconds` 不会中止正在运行的执行器。需要严格执行时限时,请在 `execute()` 内部自行实现超时或使用可取消的子进程。
550
+
527
551
  ## 模型与生命周期
528
552
 
529
553
  ### Task 抽象模型
@@ -568,21 +592,23 @@ class OrderTask(Task):
568
592
 
569
593
  ```
570
594
  pending ──(Worker 消费)──▶ running ──(执行成功)──▶ success
571
- │
572
- ├─(执行异常且重试次数用完)──▶ failed
573
- ├─(执行异常但可重试)────────▶ retry ──(重新入队)──▶ pending
574
- └─(expire_time 超时)────────▶ timeout ──(重试)────▶ retry
575
- pending ──(用户取消)────────▶ canceled(终态)
595
+ │ │
596
+ │ ├─(异常且可重试)────────▶ retry ──(重新入队并被消费)──▶ running
597
+ │ ├─(异常且重试耗尽)──────▶ failed
598
+ │ └─(执行器异常退出/锁丢失)─▶ retry 或 failed(由 reclaim 回收)
599
+ ├─(pickup 超时,从未被领取)──▶ timeout ──(RetryTimeout)──▶ retry
600
+ ├─(用户取消)────────────────▶ canceled(终态)
601
+ └─(发布最终失败)────────────▶ failed
576
602
  ```
577
603
 
578
604
  | 状态 | 含义 |
579
605
  |---|---|
580
- | `pending` | 已发布,等待 Worker 消费 |
581
- | `running` | 正在执行 |
606
+ | `pending` | 已发布,等待 Worker 消费;拒绝回退也会回到该状态 |
607
+ | `running` | 已获取执行锁,正在执行 |
582
608
  | `success` | 执行成功 |
583
- | `failed` | 重试次数耗尽后失败 |
584
- | `retry` | 失败但可重试,消息已重新入队 |
585
- | `timeout` | 超过 `expire_time` 未完成 |
609
+ | `failed` | 重试次数耗尽、发布最终失败,或孤儿回收时已无重试次数 |
610
+ | `retry` | 失败/超时后等待重新投递,消息已重新入队 |
611
+ | `timeout` | **pending** 超过 pickup 期限仍未被领取;running 不会因 `expire_time` 自动变成 timeout |
586
612
  | `canceled` | 被用户主动取消(终态,Worker 不会执行) |
587
613
 
588
614
  ### 取消任务
@@ -602,29 +628,29 @@ TaskExecution.objects.filter(
602
628
  ### 重试与超时
603
629
 
604
630
  - **重试**:`max_retries`(默认 3)控制最大重试次数,`retry_count` 累计已重试次数;重试消息重新发布到原队列
605
- - **两种超时**:
606
- - **执行超时**(`timeout_seconds`):进入 `running` 后必须在指定时间内完成,防止执行 hang 死
607
- - **Pickup 超时**(`pickup_timeout_seconds`):创建后被 Worker 消费的等待上限,也用于拒绝回退后的重新调度保护
608
- - **超时检测与恢复**:内置 `StatusCheckExecutor`(内置 Cron)将超时的 `pending`/`running` 记录标记为 `timeout`;`RetryTimeoutExecutor` 将 `timeout` 记录重新入队重试(执行超时消耗重试次数,Pickup 超时不消耗)
631
+ - **Pickup 超时**(`pickup_timeout_seconds`):**唯一由框架自动判定的超时**。`pending` 超过 `expire_time` 后由 `StatusCheckExecutor` 标记为 `timeout`,也用于拒绝回退后的重新调度保护;Pickup 超时不消耗重试次数
632
+ - **执行超时字段**(`timeout_seconds`):写入 `TaskExecution.timeout_seconds`,并在任务进入 `running` 时用于计算 `expire_time`。当前版本**不会**因该时间到达而主动中断 running 任务;如果执行器 hang 住,需在执行器内部实现超时/取消机制
633
+ - **超时检测与恢复**:`StatusCheckExecutor` 每分钟把超过 pickup 期限的 `pending` 记录标记为 `timeout`;`RetryTimeoutExecutor` 每分钟把 `timeout` 记录重新入队,实际运行过的记录消耗一次重试次数。running 孤儿任务由 crontab 基于执行锁过期回收,而不是按 `expire_time` 判超时
609
634
 
610
635
  ### 其他模型
611
636
 
612
637
  - `CronJob` — 定时任务注册表,支持代码注册与数据库覆盖(见「Cron 任务」)
613
- - `TaskExecutionArchive` — 归档记录,已完成执行归档为加密 JSONL(见配置「归档」)
614
- - `TaskExecutionStat` — 按 `executor_class` 与日期聚合的日统计
638
+ - `TaskExecutionArchive` — 归档记录,终态执行记录归档为加密 JSONL(见配置「归档」)
639
+ - `TaskExecutionStat` — 按日期、`executor_class`、`trigger_event` 聚合的日统计
615
640
 
616
641
  ## Cron 任务
617
642
 
618
643
  ### 在代码中定义 Cron 任务
619
644
 
620
- 在任意 app 的 `cronjobs.py` 文件中使用 `register_cronjob` 注册:
645
+ 在任意 app 的 `cronjobs.py` 文件中使用 `register_cronjob` 注册,并在该 app 的 `AppConfig.ready()` 中导入这个模块;框架不会自动发现各 app 下的 `cronjobs.py`:
621
646
 
622
647
  ```python
623
648
  # myapp/cronjobs.py
624
- from django_simpletask5.cronjob_registry import register_cronjob
649
+ from django_simpletask5.core.cronjob_registry import register_cronjob
625
650
 
626
651
  register_cronjob(
627
- name='health_check',
652
+ uid='health_check',
653
+ display_name='Health Check',
628
654
  cron_expression='*/5 * * * *',
629
655
  executor_class='django_simpletask5.executors.simple_request.SimpleRequestExecutor',
630
656
  context={
@@ -636,15 +662,33 @@ register_cronjob(
636
662
  )
637
663
  ```
638
664
 
665
+ ```python
666
+ # myapp/apps.py
667
+ from django.apps import AppConfig
668
+
669
+ class MyAppConfig(AppConfig):
670
+ name = 'myapp'
671
+
672
+ def ready(self):
673
+ import myapp.cronjobs # noqa: F401
674
+ ```
675
+
639
676
  ### 从代码同步到数据库
640
677
 
641
- 注册的 Cron 任务需要同步到数据库才会生效。框架默认在 `django_simpletask_crontab` 启动时自动同步(可通过 `DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC = False` 关闭),也可以手动执行:
678
+ 注册的 Cron 任务需要同步到数据库才会生效。`django_simpletask_crontab` 运行期间每个 tick 都会自动同步(默认开启,可通过 `DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC = False` 关闭自动同步),也可以手动执行:
642
679
 
643
680
  ```bash
644
681
  python manage.py django_simpletask_sync_cronjobs
645
682
  ```
646
683
 
647
- 同步后,用户可以在 Django Admin 中查看和修改 Cron 任务,被手动修改过的任务不会在后续同步中被覆盖(`is_modified_by_user` 标记保护)。
684
+ 手动同步命令使用 `sync_cronjobs(force=True)`,即使关闭了自动同步也会执行。同步后,用户可以在 Django Admin 中查看和修改 Cron 任务,被手动修改过的任务不会在后续同步中被覆盖(`is_modified_by_user` 标记保护)。单个 CronJob 创建失败只记录日志,不会中断其它任务同步。
685
+
686
+ 启用/停用(`is_active`)的所有权独立跟踪,规则与 `is_modified_by_user` 对称——**手动改过就一直归你,直到显式重置**:
687
+
688
+ - 用户**未**手动启停过的任务,`is_active` 会按代码注册值同步(例如 `simpletask_ping_pong` 注册为 `is_active=False`,默认保持停用)。
689
+ - 用户手动暂停/启用后,会置 `is_active_modified_by_user` 标记,自动同步不再覆盖该状态,但**不影响**其它定义字段(`cron_expression` 等仍照常同步)。
690
+ - 该标记不会自动清除;需要让它重新跟随代码时,使用管理后台的“从代码定义重置”(`reset_from_code`),或在编辑页取消勾选“保留手动启用状态”。
691
+ - 这样手动启停的状态可稳定保持,不会因代码默认值变化而悄悄被改写。
648
692
 
649
693
  > 注意:cron 表达式按 Django 的 `TIME_ZONE` 设置对应的本地时间解释。`USE_TZ=True` 时仍以本地时间计算触发时刻(存储为 UTC),例如 `TIME_ZONE = 'Asia/Shanghai'` 下 `30 14 * * *` 表示每天 14:30 本地时间触发。
650
694
 
@@ -652,13 +696,14 @@ python manage.py django_simpletask_sync_cronjobs
652
696
 
653
697
  | 执行器 | 说明 |
654
698
  |---|---|
655
- | `PingPongExecutor` | 健康检查,返回 `'pong'` |
699
+ | `PingPongExecutor` | 健康检查,返回 `'pong'`;内置 Cron 默认停用 |
656
700
  | `BashScriptExecutor` | 执行 Shell 脚本 |
657
701
  | `PythonScriptExecutor` | 执行 Python 代码 |
658
702
  | `SimpleRequestExecutor` | 发起 HTTP 请求 |
659
- | `StatusCheckExecutor` | 检测卡住的执行并标记超时(每 5 分钟) |
660
- | `RetryTimeoutExecutor` | 重试超时的执行(每 10 分钟) |
661
- | `ArchiveExecutor` | 归档已完成执行并生成统计(每天凌晨 2 点) |
703
+ | `StatusCheckExecutor` | 检测超过 pickup 期限、从未被领取的 `pending` 记录并标记 `timeout`(内置 Cron 每分钟) |
704
+ | `RetryTimeoutExecutor` | 将 `timeout` 记录重新入队(内置 Cron 每分钟;运行过的记录消耗一次重试) |
705
+ | `ArchiveExecutor` | 归档前一天的终态执行并生成统计(内置 Cron 每天 2:00) |
706
+ | `PurgeExpiredExecutor` | 删除超过 `ARCHIVE_RETENTION_DAYS` 的终态执行记录(内置 Cron 每天 3:00;不删除归档文件) |
662
707
 
663
708
  ## 开发与测试
664
709
 
@@ -692,6 +737,24 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
692
737
 
693
738
  ## Releases
694
739
 
740
+ ### 0.2.9
741
+
742
+ - **启用状态所有权独立跟踪** — 新增 `CronJob.is_active_modified_by_user` 字段与迁移 `0014`,将“启用/暂停状态”与“定义字段”的所有权彻底分离。用户手动启停(快捷按钮 / 批量操作 / 编辑表单)后置该标记,自动同步(`sync_cronjobs`)不再覆盖 `is_active`;未手动改过的任务仍按代码注册值同步。暂停只锁 `is_active`,`cron_expression` 等定义字段照常同步
743
+ - **显式释放所有权** — 该标记不会自动清除;需要让任务重新跟随代码时,使用管理后台“从代码定义重置”(`reset_from_code`),或在编辑页取消勾选“保留手动启用状态”
744
+ - **保留列表分页/过滤条件** — 修复暂停/运行按钮提交后重定向丢失查询串的问题:按钮表单 `action` 及视图重定向均携带当前 changelist 的 `GET` 参数,操作后刷新仍停留在原分页、过滤器与搜索条件
745
+ - **消息发布可靠性增强** — Kombu 发布复用连接池并支持 broker heartbeat;`publish()` 返回 `queue` / `message_count` / `consumer_count` 队列统计;新增可配置发布重试策略和递增重试延迟;发布异常时自动丢弃损坏连接;新增公开的 `publish_message()` 与兜底的 `safe_publish_message()`,最终发布失败会将 execution 标记为 `failed`
746
+ - **发布日志节流** — 发布、重试投递、孤儿任务回收和 CronJob 触发失败统一接入日志节流,避免 broker/Redis 长时间不可用时刷屏;重投日志同时记录队列积压与消费者数量
747
+ - **Worker 与回收健壮性** — Worker 注册表 Redis 客户端改为公开 `get_redis_client()`;修正 Worker 清理 key 的 glob 为 `worker:{pid}:*`;`reclaim._lock_config()` 与 `get_lock_manager()` 对齐,只有 `None` 才回退默认锁配置;回收扫描前后关闭旧数据库连接,减少长时间运行后的连接失效
748
+ - **日志初始化修复** — `setup_console_logging()` 不再把 `FileHandler` 误判为控制台 handler,避免已有文件日志时跳过控制台输出
749
+ - **CronJob 同步健壮性** — `sync_cronjobs(force=True)` 支持手动同步命令绕过 `DJANGO_SIMPLETASK_CRONJOB_AUTO_SYNC`;单条 CronJob 创建失败不再中断其它任务同步;`django_simpletask_sync_cronjobs` 命令改为强制同步
750
+ - **API 命名整理** — `_CRONJOB_REGISTRY` → `CRONJOB_REGISTRY`、`_compute_next_run_time()` → `compute_next_run_time()`、`_get_broker_url()` → `get_broker_url()`、`_model_to_dict()` → `model_to_dict()`、`_DeleteIntercepted` → `DeleteIntercepted`、`_get_redis_client()` → `get_redis_client()`;依赖旧私有名称的扩展或测试需同步调整
751
+ - **可观测性入口公开** — Dashboard 使用的 Worker 统计入口由 `_get_worker_stats` 改为公开的 `get_worker_stats()`;消费者线程心跳改为读取 `DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL`,Worker 注册、Redis 降级和日志节流逻辑补充了完整说明
752
+ - **注释与文档校准** — 为整个 `django_simpletask5` 生产包补充中文模块/类/函数/分支注释,并修正归档路径、归档清理、`running/timeout` 状态流转、Admin 只读性、脚本白名单、连接池 heartbeat、优雅退出等待时间等与实际行为不一致的说明
753
+ - **迁移与翻译** — 新增迁移 `django_simpletask5.0014_cronjob_is_active_modified_by_user`,同步更新简体中文翻译与迁移中的字段 `help_text`
754
+ - **测试** — 新增 `test_logging_utils.py`;扩充消息发布、消息队列、CronJob 同步、Admin 启停、Reclaim、Worker 恢复、Redis 锁与集成测试,覆盖最终发布失败、队列积压统计、`force` 同步、启停所有权、查询串保留、空锁配置和 Worker 清理 glob 等场景
755
+ - **环境要求** — 代码使用 `X | None` 联合类型,最低 Python 版本明确为 3.10;同时将 Django 最低版本明确为 4.2,并补充 `Django>=4.2` 依赖声明
756
+ - **版本** — `pyproject.toml` 版本更新为 `0.2.9`
757
+
695
758
  ### 0.2.8
696
759
 
697
760
  - **移除 `Task` 冗余的 `task_id` 字段** — `TaskExecution.task_id` 由 UUID 改为 `CharField(max_length=255)`,直接用于存放业务对象主键字符串(`str(instance.pk)`),不再维护一份与业务主键重复的 UUID。消息上下文中的 `task_id` 字段名不变,值语义从 UUID 变为业务主键字符串(如 `"42"`)