django-simpletask5 0.2.9__tar.gz → 0.2.10__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 (68) hide show
  1. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/PKG-INFO +11 -8
  2. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/README.md +10 -7
  3. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/admin.py +4 -2
  4. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/dashboards.py +101 -31
  5. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/retry_timeout.py +17 -14
  6. django_simpletask5-0.2.10/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  7. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.po +6 -0
  8. django_simpletask5-0.2.10/django_simpletask5/migrations/0015_taskexecution_idx_exec_status_created.py +19 -0
  9. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/models.py +23 -6
  10. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5.egg-info/PKG-INFO +11 -8
  11. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5.egg-info/SOURCES.txt +1 -0
  12. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/pyproject.toml +1 -1
  13. django_simpletask5-0.2.9/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  14. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/LICENSE +0 -0
  15. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/__init__.py +0 -0
  16. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/apps.py +0 -0
  17. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/__init__.py +0 -0
  18. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/cronjob_registry.py +0 -0
  19. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/defaults.py +0 -0
  20. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/executor_scanner.py +0 -0
  21. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/lock.py +0 -0
  22. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/logging_utils.py +0 -0
  23. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/message_queue.py +0 -0
  24. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/publisher.py +0 -0
  25. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/reclaim.py +0 -0
  26. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/security.py +0 -0
  27. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/signals.py +0 -0
  28. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/core/worker_registry.py +0 -0
  29. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/__init__.py +0 -0
  30. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/archive.py +0 -0
  31. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/base.py +0 -0
  32. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/bash_script.py +0 -0
  33. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/exceptions.py +0 -0
  34. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/loader.py +0 -0
  35. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/ping_pong.py +0 -0
  36. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/python_script.py +0 -0
  37. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/simple_request.py +0 -0
  38. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/executors/status_check.py +0 -0
  39. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/management/__init__.py +0 -0
  40. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/management/commands/__init__.py +0 -0
  41. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/management/commands/django_simpletask_archive.py +0 -0
  42. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/management/commands/django_simpletask_crontab.py +0 -0
  43. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/management/commands/django_simpletask_executor.py +0 -0
  44. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/management/commands/django_simpletask_queues.py +0 -0
  45. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/management/commands/django_simpletask_sync_cronjobs.py +0 -0
  46. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0001_initial.py +0 -0
  47. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0002_taskexecution_done_tasks_count_and_more.py +0 -0
  48. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0003_alter_cronjob_options_remove_cronjob_name_and_more.py +0 -0
  49. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0004_alter_cronjob_is_modified_by_user.py +0 -0
  50. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0005_alter_cronjob_is_modified_by_user_help.py +0 -0
  51. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0006_alter_cronjob_options_alter_taskexecution_created_at_and_more.py +0 -0
  52. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0007_rename_task_model_to_executor_class_in_stat.py +0 -0
  53. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0008_taskexecution_pickup_timeout_seconds.py +0 -0
  54. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0009_add_cronjob_app_label.py +0 -0
  55. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0010_taskexecution_id_alter_taskexecution_execution_id.py +0 -0
  56. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0011_taskexecutionstat_canceled_count.py +0 -0
  57. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0012_alter_taskexecution_status.py +0 -0
  58. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0013_alter_taskexecution_task_id.py +0 -0
  59. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/0014_cronjob_is_active_modified_by_user.py +0 -0
  60. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/migrations/__init__.py +0 -0
  61. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/services/__init__.py +0 -0
  62. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/services/archive.py +0 -0
  63. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/static/django_simpletask5/css/dashboard.css +0 -0
  64. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5/static/django_simpletask5/js/select2_executor.js +0 -0
  65. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5.egg-info/dependency_links.txt +0 -0
  66. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5.egg-info/requires.txt +0 -0
  67. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/django_simpletask5.egg-info/top_level.txt +0 -0
  68. {django_simpletask5-0.2.9 → django_simpletask5-0.2.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-simpletask5
3
- Version: 0.2.9
3
+ Version: 0.2.10
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>
@@ -776,6 +776,16 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
776
776
 
777
777
  ## Releases
778
778
 
779
+ ### 0.2.10
780
+
781
+ - **修复 MySQL/InnoDB 并发写入死锁** — 后台批量改状态原使用 `WHERE execution_id IN (...)`、`WHERE status = 'pending'` 等**二级索引范围更新**,会与前台 `Task` 子类 `create()` 触发的 `TaskExecution` 插入在二级索引间隙上相互阻塞,在高并发下偶发 `OperationalError (1213)` 死锁。现改为**先查出主键、再按主键更新**(`WHERE id IN (...)`),不触碰二级索引间隙,从源头消除死锁,无需重试、不改变原有同事务语义
782
+ - `django_simpletask5/executors/retry_timeout.py`:领取超时 / 执行超时的批量改状态改为按主键更新
783
+ - `django_simpletask5/admin.py`:`cancel_selected` 批量取消改为按主键更新
784
+ - **修复 MySQL 下 `bulk_create` 触发任务时 `task_id` 为空** — 部分数据库后端(如 MySQL)的 `bulk_create` 不回填自增主键,导致批量创建时生成的 `TaskExecution.task_id` 落成 `None`,业务对象无法反查。现按 `skip_publish` 分流:标记跳过发布的实例仍走高性能批量插入,需要触发任务的实例改为逐条保存,由信号在拿到真实主键后创建并发布 execution。返回值语义保持不变:仍返回传入的同一批实例并保持原顺序
785
+ - **修复示例项目 `OrderTask` 迁移的主键顺序** — 修正 `0002` 迁移先删旧主键再新增自增主键,解决 MySQL 执行时 `1068 Multiple primary key defined`
786
+ - **Dashboard 查询优化** — 卡片当日指标、近 30 天状态分布改用走索引的自然日范围查询(`created_at__gte/__lt`)替代 `created_at__date` 全表扫描;每日执行图表由逐日 60 次 `count` 合并为单次 `TruncDate` 分组聚合。仪表盘整体查询数降至常量级,不再随天数增长。新增 `TaskExecution` 的 `(status, created_at)` 联合索引(迁移 `0015`)支撑状态分布查询
787
+ - **Dashboard Worker 表增强** — Active Workers 表新增“运行时长”与“最后心跳”两列;心跳距今超过 Worker 注册 TTL 阈值(约 60%)时以 `⚠` 标记,提示该 Worker 可能即将失联;时间戳缺失或非法时降级为 `-`,不影响其余展示
788
+
779
789
  ### 0.2.9
780
790
 
781
791
  - **启用状态所有权独立跟踪** — 新增 `CronJob.is_active_modified_by_user` 字段与迁移 `0014`,将“启用/暂停状态”与“定义字段”的所有权彻底分离。用户手动启停(快捷按钮 / 批量操作 / 编辑表单)后置该标记,自动同步(`sync_cronjobs`)不再覆盖 `is_active`;未手动改过的任务仍按代码注册值同步。暂停只锁 `is_active`,`cron_expression` 等定义字段照常同步
@@ -790,9 +800,7 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
790
800
  - **可观测性入口公开** — Dashboard 使用的 Worker 统计入口由 `_get_worker_stats` 改为公开的 `get_worker_stats()`;消费者线程心跳改为读取 `DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL`,Worker 注册、Redis 降级和日志节流逻辑补充了完整说明
791
801
  - **注释与文档校准** — 为整个 `django_simpletask5` 生产包补充中文模块/类/函数/分支注释,并修正归档路径、归档清理、`running/timeout` 状态流转、Admin 只读性、脚本白名单、连接池 heartbeat、优雅退出等待时间等与实际行为不一致的说明
792
802
  - **迁移与翻译** — 新增迁移 `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
803
  - **环境要求** — 代码使用 `X | None` 联合类型,最低 Python 版本明确为 3.10;同时将 Django 最低版本明确为 4.2,并补充 `Django>=4.2` 依赖声明
795
- - **版本** — `pyproject.toml` 版本更新为 `0.2.9`
796
804
 
797
805
  ### 0.2.8
798
806
 
@@ -807,15 +815,11 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
807
815
  - **重试不再搁浅任务** — 失败重试先落库 `retry` 再发布重试消息,发布失败则 requeue 当前消息,杜绝任务永久停在无人处理的 `retry` 状态;KombuQueue 依据 AMQP `x-death` 上限丢弃毒消息,避免无限 requeue 造成的队头阻塞与空转
808
816
  - **消费者重连提示** — Worker 断连重连成功后输出 `Consumer connection restored` 恢复日志,便于运维确认消费通道已恢复
809
817
  - **安全与健壮性加固** — broker 账号/密码按百分号编码、脚本白名单按路径段边界匹配、signals 快照按实例隔离,覆盖相关回归测试
810
- - **新增测试约定** — 单元测试中对预期触发(如无效 UUID 校验、毒消息丢弃、消费者断连、执行器加载失败等)产生的错误日志必须屏蔽输出,统一使用 `django_simpletask5_example.tests.suppress_expected_errors` 或替换 logger,保证测试输出干净可读;本次同步清理全部此类预期错误日志
811
818
 
812
819
  ### 0.2.6
813
820
 
814
821
  - **新增 `canceled` 状态** — 取消已发布但未执行的任务改为独立的终态 `canceled`(原复用 `failed`),Worker 消费到已取消消息时直接跳过;Admin 取消操作、归档/清理/统计(新增 `canceled_count`)、Dashboard 状态图均纳入该状态,取消与执行失败可清晰区分
815
822
  - **WorkerSupervisor 守护线程** — 统一的 Worker 线程保活机制,周期性心跳并自动重启异常退出的 Worker 线程,保持目标 Worker 数量,提升长时间运行稳定性
816
- - **韧性/恢复测试套件** — 新增 `test_resilience_recovery.py`,模拟 Redis、DB、MQ 连接中断后的自动恢复:Worker 启动时 Redis 注册失败重试、消费者断连自动重连、锁引擎中断后消息重入队并在 Redis 恢复后继续处理、DB 瞬断重入队、执行器异常标记 failed/retry、crontab 调度器 Redis 恢复检测等
817
- - **测试套件稳定性修复** — `test_worker_registry` 隔离全局 `_active_workers` 消除跨用例污染;`test_integration` 动态探测本地可用的 Redis/RabbitMQ 镜像(支持 `DJANGO_SIMPLETASK_REDIS_IMAGE` / `DJANGO_SIMPLETASK_RABBITMQ_IMAGE` 环境变量覆盖),并改为真实 AMQP 握手探测 RabbitMQ 就绪,避免新建容器连接被重置的偶发失败
818
- - **测试服务脚本** — `start-test-services.sh` 默认使用 6379/5672 端口并动态检测本地镜像,提供 `start` / `stop` / `status` 子命令,便于一键启动测试依赖
819
823
 
820
824
  ### 0.2.5
821
825
 
@@ -860,7 +864,6 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
860
864
  - **破坏性变更**: 移除 `Task` 模型上的自定义主键,`task_id` 不再是主键字段,改为 `unique=True` 的唯一标识字段。所有 `Task` 子模型将自动获得 Django 默认的自增 `id` 主键。已有数据库需要迁移处理。
861
865
  - **统计维度调整**: `TaskExecutionStat` 的统计维度由 `task_model` 改为 `executor_class`,统一覆盖有模型任务和 Cron 定时任务两种场景。对应 API `_update_stats()` 按 `executor_class` 分组聚合。
862
866
  - **Admin 优化**: CronJob 列表页移除 `executor_class` 列避免表格撑开;TaskExecutionStat 列表页新增 `executor_class` 列、`created_at` 设为只读修复详情页错误。
863
- - **测试修复**: 修复 `TransactionTestCase` + `on_commit` 在 SQLite 共享内存连接下因 `TestCase` 残留原子块导致回调不执行的兼容性问题;Redis MQ / RabbitMQ E2E 集成测试全部通过。
864
867
 
865
868
  ### 0.1.5
866
869
 
@@ -737,6 +737,16 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
737
737
 
738
738
  ## Releases
739
739
 
740
+ ### 0.2.10
741
+
742
+ - **修复 MySQL/InnoDB 并发写入死锁** — 后台批量改状态原使用 `WHERE execution_id IN (...)`、`WHERE status = 'pending'` 等**二级索引范围更新**,会与前台 `Task` 子类 `create()` 触发的 `TaskExecution` 插入在二级索引间隙上相互阻塞,在高并发下偶发 `OperationalError (1213)` 死锁。现改为**先查出主键、再按主键更新**(`WHERE id IN (...)`),不触碰二级索引间隙,从源头消除死锁,无需重试、不改变原有同事务语义
743
+ - `django_simpletask5/executors/retry_timeout.py`:领取超时 / 执行超时的批量改状态改为按主键更新
744
+ - `django_simpletask5/admin.py`:`cancel_selected` 批量取消改为按主键更新
745
+ - **修复 MySQL 下 `bulk_create` 触发任务时 `task_id` 为空** — 部分数据库后端(如 MySQL)的 `bulk_create` 不回填自增主键,导致批量创建时生成的 `TaskExecution.task_id` 落成 `None`,业务对象无法反查。现按 `skip_publish` 分流:标记跳过发布的实例仍走高性能批量插入,需要触发任务的实例改为逐条保存,由信号在拿到真实主键后创建并发布 execution。返回值语义保持不变:仍返回传入的同一批实例并保持原顺序
746
+ - **修复示例项目 `OrderTask` 迁移的主键顺序** — 修正 `0002` 迁移先删旧主键再新增自增主键,解决 MySQL 执行时 `1068 Multiple primary key defined`
747
+ - **Dashboard 查询优化** — 卡片当日指标、近 30 天状态分布改用走索引的自然日范围查询(`created_at__gte/__lt`)替代 `created_at__date` 全表扫描;每日执行图表由逐日 60 次 `count` 合并为单次 `TruncDate` 分组聚合。仪表盘整体查询数降至常量级,不再随天数增长。新增 `TaskExecution` 的 `(status, created_at)` 联合索引(迁移 `0015`)支撑状态分布查询
748
+ - **Dashboard Worker 表增强** — Active Workers 表新增“运行时长”与“最后心跳”两列;心跳距今超过 Worker 注册 TTL 阈值(约 60%)时以 `⚠` 标记,提示该 Worker 可能即将失联;时间戳缺失或非法时降级为 `-`,不影响其余展示
749
+
740
750
  ### 0.2.9
741
751
 
742
752
  - **启用状态所有权独立跟踪** — 新增 `CronJob.is_active_modified_by_user` 字段与迁移 `0014`,将“启用/暂停状态”与“定义字段”的所有权彻底分离。用户手动启停(快捷按钮 / 批量操作 / 编辑表单)后置该标记,自动同步(`sync_cronjobs`)不再覆盖 `is_active`;未手动改过的任务仍按代码注册值同步。暂停只锁 `is_active`,`cron_expression` 等定义字段照常同步
@@ -751,9 +761,7 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
751
761
  - **可观测性入口公开** — Dashboard 使用的 Worker 统计入口由 `_get_worker_stats` 改为公开的 `get_worker_stats()`;消费者线程心跳改为读取 `DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL`,Worker 注册、Redis 降级和日志节流逻辑补充了完整说明
752
762
  - **注释与文档校准** — 为整个 `django_simpletask5` 生产包补充中文模块/类/函数/分支注释,并修正归档路径、归档清理、`running/timeout` 状态流转、Admin 只读性、脚本白名单、连接池 heartbeat、优雅退出等待时间等与实际行为不一致的说明
753
763
  - **迁移与翻译** — 新增迁移 `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
764
  - **环境要求** — 代码使用 `X | None` 联合类型,最低 Python 版本明确为 3.10;同时将 Django 最低版本明确为 4.2,并补充 `Django>=4.2` 依赖声明
756
- - **版本** — `pyproject.toml` 版本更新为 `0.2.9`
757
765
 
758
766
  ### 0.2.8
759
767
 
@@ -768,15 +776,11 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
768
776
  - **重试不再搁浅任务** — 失败重试先落库 `retry` 再发布重试消息,发布失败则 requeue 当前消息,杜绝任务永久停在无人处理的 `retry` 状态;KombuQueue 依据 AMQP `x-death` 上限丢弃毒消息,避免无限 requeue 造成的队头阻塞与空转
769
777
  - **消费者重连提示** — Worker 断连重连成功后输出 `Consumer connection restored` 恢复日志,便于运维确认消费通道已恢复
770
778
  - **安全与健壮性加固** — broker 账号/密码按百分号编码、脚本白名单按路径段边界匹配、signals 快照按实例隔离,覆盖相关回归测试
771
- - **新增测试约定** — 单元测试中对预期触发(如无效 UUID 校验、毒消息丢弃、消费者断连、执行器加载失败等)产生的错误日志必须屏蔽输出,统一使用 `django_simpletask5_example.tests.suppress_expected_errors` 或替换 logger,保证测试输出干净可读;本次同步清理全部此类预期错误日志
772
779
 
773
780
  ### 0.2.6
774
781
 
775
782
  - **新增 `canceled` 状态** — 取消已发布但未执行的任务改为独立的终态 `canceled`(原复用 `failed`),Worker 消费到已取消消息时直接跳过;Admin 取消操作、归档/清理/统计(新增 `canceled_count`)、Dashboard 状态图均纳入该状态,取消与执行失败可清晰区分
776
783
  - **WorkerSupervisor 守护线程** — 统一的 Worker 线程保活机制,周期性心跳并自动重启异常退出的 Worker 线程,保持目标 Worker 数量,提升长时间运行稳定性
777
- - **韧性/恢复测试套件** — 新增 `test_resilience_recovery.py`,模拟 Redis、DB、MQ 连接中断后的自动恢复:Worker 启动时 Redis 注册失败重试、消费者断连自动重连、锁引擎中断后消息重入队并在 Redis 恢复后继续处理、DB 瞬断重入队、执行器异常标记 failed/retry、crontab 调度器 Redis 恢复检测等
778
- - **测试套件稳定性修复** — `test_worker_registry` 隔离全局 `_active_workers` 消除跨用例污染;`test_integration` 动态探测本地可用的 Redis/RabbitMQ 镜像(支持 `DJANGO_SIMPLETASK_REDIS_IMAGE` / `DJANGO_SIMPLETASK_RABBITMQ_IMAGE` 环境变量覆盖),并改为真实 AMQP 握手探测 RabbitMQ 就绪,避免新建容器连接被重置的偶发失败
779
- - **测试服务脚本** — `start-test-services.sh` 默认使用 6379/5672 端口并动态检测本地镜像,提供 `start` / `stop` / `status` 子命令,便于一键启动测试依赖
780
784
 
781
785
  ### 0.2.5
782
786
 
@@ -821,7 +825,6 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
821
825
  - **破坏性变更**: 移除 `Task` 模型上的自定义主键,`task_id` 不再是主键字段,改为 `unique=True` 的唯一标识字段。所有 `Task` 子模型将自动获得 Django 默认的自增 `id` 主键。已有数据库需要迁移处理。
822
826
  - **统计维度调整**: `TaskExecutionStat` 的统计维度由 `task_model` 改为 `executor_class`,统一覆盖有模型任务和 Cron 定时任务两种场景。对应 API `_update_stats()` 按 `executor_class` 分组聚合。
823
827
  - **Admin 优化**: CronJob 列表页移除 `executor_class` 列避免表格撑开;TaskExecutionStat 列表页新增 `executor_class` 列、`created_at` 设为只读修复详情页错误。
824
- - **测试修复**: 修复 `TransactionTestCase` + `on_commit` 在 SQLite 共享内存连接下因 `TestCase` 残留原子块导致回调不执行的兼容性问题;Redis MQ / RabbitMQ E2E 集成测试全部通过。
825
828
 
826
829
  ### 0.1.5
827
830
 
@@ -279,8 +279,10 @@ class TaskExecutionAdmin(DjangoCheckboxNormalizeAdmin, DjangoTabbedChangeformAdm
279
279
  @admin.action(description=_("Cancel selected pending tasks"))
280
280
  def cancel_selected(self, request, queryset):
281
281
  """批量取消仍处于 pending 的记录。"""
282
- # 用 queryset.update 一次完成,避免逐条 save 产生大量 SQL。
283
- updated = queryset.filter(status='pending').update(status='canceled')
282
+ # 先按状态筛出主键,再按主键批量更新:避免直接对 status 二级索引
283
+ # 做范围 UPDATE 产生间隙锁,与前台 create 的 execution INSERT 死锁。
284
+ pks = list(queryset.filter(status='pending').values_list('pk', flat=True))
285
+ updated = TaskExecution.objects.filter(pk__in=pks).update(status='canceled') if pks else 0
284
286
  self.message_user(request, _("%d execution(s) have been cancelled.") % updated)
285
287
 
286
288
 
@@ -1,15 +1,46 @@
1
1
  """Admin 仪表盘:执行统计、趋势图、失败列表与 Worker 状态。"""
2
2
 
3
- from datetime import date, timedelta
3
+ import time
4
+ from datetime import date, datetime, time as dt_time, timedelta
4
5
 
5
- from django.db.models import Count
6
+ from django.db.models import Count, Q
7
+ from django.db.models.functions import TruncDate
6
8
  from django.urls import reverse
9
+ from django.utils import timezone
7
10
  from django.utils.translation import gettext_lazy as _
8
11
 
9
12
  from django_admin_dashboards.base import Dashboard, Layout, CardComponent, ChartComponent, TableComponent
10
13
 
11
14
  from django_simpletask5.models import TaskExecution, CronJob, TaskExecutionArchive
12
- from django_simpletask5.core.worker_registry import get_worker_stats
15
+ from django_simpletask5.core.worker_registry import get_worker_stats, WORKER_TTL
16
+
17
+
18
+ def _day_range(day):
19
+ """返回某自然日的 ``[起, 止)`` 时间范围,供走索引的范围查询使用。
20
+
21
+ ``created_at__date=day`` 会把列包成函数、无法命中 ``created_at`` 索引;
22
+ 改用 ``created_at__gte / __lt`` 的闭开区间即可正常走索引。
23
+ """
24
+ start = timezone.make_aware(datetime.combine(day, dt_time.min))
25
+ end = timezone.make_aware(datetime.combine(day + timedelta(days=1), dt_time.min))
26
+ return start, end
27
+
28
+
29
+ def _format_duration(seconds):
30
+ """把秒数格式化为 ``1d2h`` / ``3h05m`` / ``12m30s`` / ``45s`` 形式。"""
31
+ seconds = int(seconds)
32
+ if seconds < 0:
33
+ seconds = 0
34
+ days, rem = divmod(seconds, 86400)
35
+ hours, rem = divmod(rem, 3600)
36
+ minutes, secs = divmod(rem, 60)
37
+ if days:
38
+ return f"{days}d{hours}h"
39
+ if hours:
40
+ return f"{hours}h{minutes:02d}m"
41
+ if minutes:
42
+ return f"{minutes}m{secs:02d}s"
43
+ return f"{secs}s"
13
44
 
14
45
 
15
46
  class TaskDashboard(Dashboard):
@@ -42,21 +73,27 @@ class TaskDashboard(Dashboard):
42
73
  layout = Layout(columns=12)
43
74
 
44
75
  today = date.today()
76
+ today_start, today_end = _day_range(today)
45
77
 
46
- # 顶部卡片指标:当天执行量、成功数、失败数、积压与定时任务数。
47
- total_today = TaskExecution.objects.filter(
48
- created_at__date=today
49
- ).count()
50
- success_today = TaskExecution.objects.filter(
51
- created_at__date=today, status="success"
52
- ).count()
78
+ # 当日各状态计数合并为一次聚合查询,走 created_at 索引范围扫描。
79
+ today_stats = TaskExecution.objects.filter(
80
+ created_at__gte=today_start, created_at__lt=today_end
81
+ ).aggregate(
82
+ total=Count("execution_id"),
83
+ success=Count("execution_id", filter=Q(status="success")),
84
+ failed=Count("execution_id", filter=Q(status="failed")),
85
+ retry=Count("execution_id", filter=Q(status="retry")),
86
+ )
87
+ total_today = today_stats["total"]
88
+ success_today = today_stats["success"]
89
+ failed_today = today_stats["failed"]
90
+ retry_today = today_stats["retry"]
91
+
92
+ # 积压/运行中与定时任务、归档数:各自单次计数。
53
93
  pending = TaskExecution.objects.filter(status="pending").count()
54
94
  running = TaskExecution.objects.filter(status="running").count()
55
95
  active_cronjobs = CronJob.objects.filter(is_active=True).count()
56
96
  archives_count = TaskExecutionArchive.objects.count()
57
- failed_today = TaskExecution.objects.filter(
58
- created_at__date=today, status="failed"
59
- ).count()
60
97
 
61
98
  # Worker 数据来自 Redis;Redis 不可用时显示 "-" 而不是 0,避免误导。
62
99
  worker_stats = get_worker_stats()
@@ -122,7 +159,7 @@ class TaskDashboard(Dashboard):
122
159
  ), 3),
123
160
  (CardComponent(
124
161
  title=_("Today Retries"),
125
- value=TaskExecution.objects.filter(status="retry", created_at__date=today).count(),
162
+ value=retry_today,
126
163
  icon_class="ri-refresh-line",
127
164
  color="info",
128
165
  link=reverse("admin:django_simpletask5_taskexecution_changelist") + "?status__exact=retry",
@@ -131,8 +168,9 @@ class TaskDashboard(Dashboard):
131
168
 
132
169
  # 最近约 30 天状态分布(含今天与 30 天前):按 status 聚合执行数量。
133
170
  thirty_days_ago = today - timedelta(days=30)
171
+ thirty_days_start, _unused_end = _day_range(thirty_days_ago)
134
172
  status_counts = TaskExecution.objects.filter(
135
- created_at__date__gte=thirty_days_ago
173
+ created_at__gte=thirty_days_start
136
174
  ).values("status").annotate(
137
175
  count=Count("execution_id")
138
176
  )
@@ -228,23 +266,46 @@ class TaskDashboard(Dashboard):
228
266
  ])
229
267
 
230
268
  # Worker 列表:Redis 不可用时列表为空,但其它卡片仍可正常展示。
269
+ now = time.time()
231
270
  worker_rows = []
232
271
  for w in worker_stats["workers"]:
233
272
  pid = w.get("pid", "?")
234
273
  tid = w.get("tid", "?")
235
274
  hostname = w.get("hostname", "?")
275
+
276
+ # 运行时长:worker 自注册以来的存活时间。
277
+ try:
278
+ uptime = _format_duration(now - float(w.get("started_at", now)))
279
+ except (TypeError, ValueError):
280
+ uptime = "-"
281
+
282
+ # 最后心跳:距今时长;接近 TTL 说明 worker 可能即将失联。
283
+ try:
284
+ since_hb = now - float(w.get("updated_at", now))
285
+ stale = since_hb >= WORKER_TTL * 0.6
286
+ heartbeat = _format_duration(since_hb)
287
+ if stale:
288
+ heartbeat = f"⚠ {heartbeat}"
289
+ except (TypeError, ValueError):
290
+ heartbeat = "-"
291
+
236
292
  worker_rows.append([
237
293
  f"{hostname}:{pid}:{tid}",
238
294
  w.get("queue", "?"),
239
295
  w.get("status", "?"),
240
296
  w.get("current_execution_id", "")[:8] or "-",
241
297
  ",".join(w.get("services", [])) or "-",
298
+ uptime,
299
+ heartbeat,
242
300
  ])
243
301
 
244
302
  layout.add_row([
245
303
  (TableComponent(
246
304
  title=_("Active Workers"),
247
- columns=[_("Identity"), _("Queue"), _("Status"), _("Current Execution"), _("Services")],
305
+ columns=[
306
+ _("Identity"), _("Queue"), _("Status"), _("Current Execution"),
307
+ _("Services"), _("Uptime"), _("Last Heartbeat"),
308
+ ],
248
309
  data=worker_rows,
249
310
  ), 12),
250
311
  ])
@@ -252,28 +313,37 @@ class TaskDashboard(Dashboard):
252
313
  return layout
253
314
 
254
315
  def _daily_executions_chart(self):
255
- """构造最近 30 天成功/失败堆叠柱状图数据。"""
316
+ """构造最近 30 天成功/失败堆叠柱状图数据。
317
+
318
+ 仅执行一次按天分组的聚合查询,避免逐日发起 60 次 count。
319
+ """
256
320
  today = date.today()
321
+ start_day = today - timedelta(days=29)
322
+ start, _unused_start_end = _day_range(start_day)
323
+ _unused_today_start, end = _day_range(today)
324
+
325
+ # 一次聚合出每天的成功数、失败/超时数。
326
+ rows = TaskExecution.objects.filter(
327
+ created_at__gte=start, created_at__lt=end
328
+ ).annotate(
329
+ day=TruncDate("created_at")
330
+ ).values("day").annotate(
331
+ success=Count("execution_id", filter=Q(status="success")),
332
+ failed=Count("execution_id", filter=Q(status__in=["failed", "timeout"])),
333
+ )
334
+
335
+ by_day = {row["day"]: row for row in rows}
336
+
257
337
  labels = []
258
338
  success_data = []
259
339
  failed_data = []
260
-
261
- # 从 29 天前到今天,保证 X 轴时间正序。
340
+ # 从 29 天前到今天,保证 X 轴时间正序;缺失的日期补 0。
262
341
  for i in range(29, -1, -1):
263
342
  d = today - timedelta(days=i)
264
343
  labels.append(d.strftime("%m-%d"))
265
-
266
- # 每天两个聚合查询:成功数、失败/超时数。
267
- success_data.append(
268
- TaskExecution.objects.filter(
269
- created_at__date=d, status="success"
270
- ).count()
271
- )
272
- failed_data.append(
273
- TaskExecution.objects.filter(
274
- created_at__date=d, status__in=["failed", "timeout"]
275
- ).count()
276
- )
344
+ row = by_day.get(d)
345
+ success_data.append(row["success"] if row else 0)
346
+ failed_data.append(row["failed"] if row else 0)
277
347
  return {
278
348
  "labels": labels,
279
349
  "datasets": [
@@ -67,40 +67,43 @@ class RetryTimeoutExecutor(BaseExecutor):
67
67
  )
68
68
 
69
69
  # 领取超时:任务从未开始运行,说明消息可能丢失,不应消耗业务重试次数。
70
- pickup_ids = list(
70
+ # 同时取出主键与 execution_id:更新走主键精确命中,避免 WHERE
71
+ # execution_id IN (...) 走唯一索引范围扫描产生间隙锁,与前台 create 的
72
+ # execution INSERT 相互阻塞形成死锁。
73
+ pickup_rows = list(
71
74
  timeout_records.filter(started_at__isnull=True)
72
- .values_list('execution_id', flat=True)
75
+ .values_list('pk', 'execution_id')
73
76
  )
74
- if pickup_ids:
75
- # 先批量改状态,再在事务提交后逐条发布,避免消费者读到旧状态。
76
- TaskExecution.objects.filter(execution_id__in=pickup_ids).update(
77
+ if pickup_rows:
78
+ # 先按主键批量改状态,再在事务提交后逐条发布,避免消费者读到旧状态。
79
+ TaskExecution.objects.filter(pk__in=[pk for pk, _ in pickup_rows]).update(
77
80
  status='retry',
78
81
  )
79
- for exec_id in pickup_ids:
82
+ for _, exec_id in pickup_rows:
80
83
  transaction.on_commit(
81
84
  partial(_publish_message_by_id, exec_id)
82
85
  )
83
86
 
84
87
  # 执行超时:任务确实运行过,消耗一次重试次数。
85
- exec_ids = list(
88
+ exec_rows = list(
86
89
  timeout_records.filter(started_at__isnull=False)
87
- .values_list('execution_id', flat=True)
90
+ .values_list('pk', 'execution_id')
88
91
  )
89
92
  # 有执行超时任务时,消耗一次重试次数后重新投递。
90
- if exec_ids:
91
- TaskExecution.objects.filter(execution_id__in=exec_ids).update(
93
+ if exec_rows:
94
+ TaskExecution.objects.filter(pk__in=[pk for pk, _ in exec_rows]).update(
92
95
  status='retry',
93
96
  retry_count=models.F('retry_count') + 1,
94
97
  )
95
- for exec_id in exec_ids:
98
+ for _, exec_id in exec_rows:
96
99
  transaction.on_commit(
97
100
  partial(_publish_message_by_id, exec_id)
98
101
  )
99
102
 
100
- total = len(pickup_ids) + len(exec_ids)
103
+ total = len(pickup_rows) + len(exec_rows)
101
104
  return json.dumps({
102
105
  'retried': total,
103
- 'pickup_timeout_retried': len(pickup_ids),
104
- 'exec_timeout_retried': len(exec_ids),
106
+ 'pickup_timeout_retried': len(pickup_rows),
107
+ 'exec_timeout_retried': len(exec_rows),
105
108
  'success': True,
106
109
  })
@@ -243,6 +243,12 @@ msgstr "当前执行"
243
243
  msgid "Services"
244
244
  msgstr "服务"
245
245
 
246
+ msgid "Uptime"
247
+ msgstr "运行时长"
248
+
249
+ msgid "Last Heartbeat"
250
+ msgstr "最后心跳"
251
+
246
252
  #: django_simpletask5/dashboards.py:261
247
253
  msgid "Failed/Timeout"
248
254
  msgstr "失败/超时"
@@ -0,0 +1,19 @@
1
+ # Generated by Django 5.2.17 on 2026-10-27 06:17
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("django_simpletask5", "0014_cronjob_is_active_modified_by_user"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddIndex(
14
+ model_name="taskexecution",
15
+ index=models.Index(
16
+ fields=["status", "created_at"], name="idx_exec_status_created"
17
+ ),
18
+ ),
19
+ ]
@@ -54,13 +54,29 @@ class TaskQuerySet(models.QuerySet):
54
54
  """Task 专用 QuerySet,在批量创建后自动发布任务。"""
55
55
 
56
56
  def bulk_create(self, objs, **kwargs):
57
- """批量创建业务对象,并为创建出的实例批量生成 execution。"""
58
- instances = super().bulk_create(objs, **kwargs)
57
+ """批量创建业务对象,并按需为创建出的实例生成 execution。
59
58
 
60
- # 延迟导入避免 models 与 publisher 的循环依赖。
61
- from django_simpletask5.core.publisher import bulk_create_and_publish
62
- bulk_create_and_publish(instances, trigger_event='create')
63
- return instances
59
+ 需要触发消息的实例逐条 ``save()``,由 post_save 信号发布任务。这样做的
60
+ 原因是部分数据库后端(如 MySQL)的 ``bulk_create`` 不会回填自增主键,
61
+ 会导致 execution 的 ``task_id`` 落成 None;逐条保存可拿到真实主键。
62
+
63
+ 标记了 ``skip_publish`` 的实例不触发任务,仍走高性能的 ``bulk_create``。
64
+ 返回值与 Django 一致:传入的同一批实例(保持原顺序)。
65
+ """
66
+ objs = list(objs)
67
+
68
+ # 屏蔽消息触发的实例不产生 execution,保留批量插入的高性能。
69
+ skipped = [obj for obj in objs if getattr(obj, '_skip_publish', False)]
70
+ if skipped:
71
+ super().bulk_create(skipped, **kwargs)
72
+
73
+ # 需要触发的实例逐条保存:信号会在 post_save 中创建 execution 并发布。
74
+ # bulk_create 专属的 kwargs(batch_size/ignore_conflicts 等)在此无意义,忽略。
75
+ for obj in objs:
76
+ if not getattr(obj, '_skip_publish', False):
77
+ obj.save()
78
+
79
+ return objs
64
80
 
65
81
 
66
82
  class TaskManager(models.Manager):
@@ -335,6 +351,7 @@ class TaskExecution(models.Model):
335
351
  ordering = ['-created_at']
336
352
  indexes = [
337
353
  models.Index(fields=['status', 'expire_time'], name='idx_exec_status_expire'),
354
+ models.Index(fields=['status', 'created_at'], name='idx_exec_status_created'),
338
355
  ]
339
356
 
340
357
  def __str__(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-simpletask5
3
- Version: 0.2.9
3
+ Version: 0.2.10
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>
@@ -776,6 +776,16 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
776
776
 
777
777
  ## Releases
778
778
 
779
+ ### 0.2.10
780
+
781
+ - **修复 MySQL/InnoDB 并发写入死锁** — 后台批量改状态原使用 `WHERE execution_id IN (...)`、`WHERE status = 'pending'` 等**二级索引范围更新**,会与前台 `Task` 子类 `create()` 触发的 `TaskExecution` 插入在二级索引间隙上相互阻塞,在高并发下偶发 `OperationalError (1213)` 死锁。现改为**先查出主键、再按主键更新**(`WHERE id IN (...)`),不触碰二级索引间隙,从源头消除死锁,无需重试、不改变原有同事务语义
782
+ - `django_simpletask5/executors/retry_timeout.py`:领取超时 / 执行超时的批量改状态改为按主键更新
783
+ - `django_simpletask5/admin.py`:`cancel_selected` 批量取消改为按主键更新
784
+ - **修复 MySQL 下 `bulk_create` 触发任务时 `task_id` 为空** — 部分数据库后端(如 MySQL)的 `bulk_create` 不回填自增主键,导致批量创建时生成的 `TaskExecution.task_id` 落成 `None`,业务对象无法反查。现按 `skip_publish` 分流:标记跳过发布的实例仍走高性能批量插入,需要触发任务的实例改为逐条保存,由信号在拿到真实主键后创建并发布 execution。返回值语义保持不变:仍返回传入的同一批实例并保持原顺序
785
+ - **修复示例项目 `OrderTask` 迁移的主键顺序** — 修正 `0002` 迁移先删旧主键再新增自增主键,解决 MySQL 执行时 `1068 Multiple primary key defined`
786
+ - **Dashboard 查询优化** — 卡片当日指标、近 30 天状态分布改用走索引的自然日范围查询(`created_at__gte/__lt`)替代 `created_at__date` 全表扫描;每日执行图表由逐日 60 次 `count` 合并为单次 `TruncDate` 分组聚合。仪表盘整体查询数降至常量级,不再随天数增长。新增 `TaskExecution` 的 `(status, created_at)` 联合索引(迁移 `0015`)支撑状态分布查询
787
+ - **Dashboard Worker 表增强** — Active Workers 表新增“运行时长”与“最后心跳”两列;心跳距今超过 Worker 注册 TTL 阈值(约 60%)时以 `⚠` 标记,提示该 Worker 可能即将失联;时间戳缺失或非法时降级为 `-`,不影响其余展示
788
+
779
789
  ### 0.2.9
780
790
 
781
791
  - **启用状态所有权独立跟踪** — 新增 `CronJob.is_active_modified_by_user` 字段与迁移 `0014`,将“启用/暂停状态”与“定义字段”的所有权彻底分离。用户手动启停(快捷按钮 / 批量操作 / 编辑表单)后置该标记,自动同步(`sync_cronjobs`)不再覆盖 `is_active`;未手动改过的任务仍按代码注册值同步。暂停只锁 `is_active`,`cron_expression` 等定义字段照常同步
@@ -790,9 +800,7 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
790
800
  - **可观测性入口公开** — Dashboard 使用的 Worker 统计入口由 `_get_worker_stats` 改为公开的 `get_worker_stats()`;消费者线程心跳改为读取 `DJANGO_SIMPLETASK_WORKER_HEARTBEAT_INTERVAL`,Worker 注册、Redis 降级和日志节流逻辑补充了完整说明
791
801
  - **注释与文档校准** — 为整个 `django_simpletask5` 生产包补充中文模块/类/函数/分支注释,并修正归档路径、归档清理、`running/timeout` 状态流转、Admin 只读性、脚本白名单、连接池 heartbeat、优雅退出等待时间等与实际行为不一致的说明
792
802
  - **迁移与翻译** — 新增迁移 `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
803
  - **环境要求** — 代码使用 `X | None` 联合类型,最低 Python 版本明确为 3.10;同时将 Django 最低版本明确为 4.2,并补充 `Django>=4.2` 依赖声明
795
- - **版本** — `pyproject.toml` 版本更新为 `0.2.9`
796
804
 
797
805
  ### 0.2.8
798
806
 
@@ -807,15 +815,11 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
807
815
  - **重试不再搁浅任务** — 失败重试先落库 `retry` 再发布重试消息,发布失败则 requeue 当前消息,杜绝任务永久停在无人处理的 `retry` 状态;KombuQueue 依据 AMQP `x-death` 上限丢弃毒消息,避免无限 requeue 造成的队头阻塞与空转
808
816
  - **消费者重连提示** — Worker 断连重连成功后输出 `Consumer connection restored` 恢复日志,便于运维确认消费通道已恢复
809
817
  - **安全与健壮性加固** — broker 账号/密码按百分号编码、脚本白名单按路径段边界匹配、signals 快照按实例隔离,覆盖相关回归测试
810
- - **新增测试约定** — 单元测试中对预期触发(如无效 UUID 校验、毒消息丢弃、消费者断连、执行器加载失败等)产生的错误日志必须屏蔽输出,统一使用 `django_simpletask5_example.tests.suppress_expected_errors` 或替换 logger,保证测试输出干净可读;本次同步清理全部此类预期错误日志
811
818
 
812
819
  ### 0.2.6
813
820
 
814
821
  - **新增 `canceled` 状态** — 取消已发布但未执行的任务改为独立的终态 `canceled`(原复用 `failed`),Worker 消费到已取消消息时直接跳过;Admin 取消操作、归档/清理/统计(新增 `canceled_count`)、Dashboard 状态图均纳入该状态,取消与执行失败可清晰区分
815
822
  - **WorkerSupervisor 守护线程** — 统一的 Worker 线程保活机制,周期性心跳并自动重启异常退出的 Worker 线程,保持目标 Worker 数量,提升长时间运行稳定性
816
- - **韧性/恢复测试套件** — 新增 `test_resilience_recovery.py`,模拟 Redis、DB、MQ 连接中断后的自动恢复:Worker 启动时 Redis 注册失败重试、消费者断连自动重连、锁引擎中断后消息重入队并在 Redis 恢复后继续处理、DB 瞬断重入队、执行器异常标记 failed/retry、crontab 调度器 Redis 恢复检测等
817
- - **测试套件稳定性修复** — `test_worker_registry` 隔离全局 `_active_workers` 消除跨用例污染;`test_integration` 动态探测本地可用的 Redis/RabbitMQ 镜像(支持 `DJANGO_SIMPLETASK_REDIS_IMAGE` / `DJANGO_SIMPLETASK_RABBITMQ_IMAGE` 环境变量覆盖),并改为真实 AMQP 握手探测 RabbitMQ 就绪,避免新建容器连接被重置的偶发失败
818
- - **测试服务脚本** — `start-test-services.sh` 默认使用 6379/5672 端口并动态检测本地镜像,提供 `start` / `stop` / `status` 子命令,便于一键启动测试依赖
819
823
 
820
824
  ### 0.2.5
821
825
 
@@ -860,7 +864,6 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
860
864
  - **破坏性变更**: 移除 `Task` 模型上的自定义主键,`task_id` 不再是主键字段,改为 `unique=True` 的唯一标识字段。所有 `Task` 子模型将自动获得 Django 默认的自增 `id` 主键。已有数据库需要迁移处理。
861
865
  - **统计维度调整**: `TaskExecutionStat` 的统计维度由 `task_model` 改为 `executor_class`,统一覆盖有模型任务和 Cron 定时任务两种场景。对应 API `_update_stats()` 按 `executor_class` 分组聚合。
862
866
  - **Admin 优化**: CronJob 列表页移除 `executor_class` 列避免表格撑开;TaskExecutionStat 列表页新增 `executor_class` 列、`created_at` 设为只读修复详情页错误。
863
- - **测试修复**: 修复 `TransactionTestCase` + `on_commit` 在 SQLite 共享内存连接下因 `TestCase` 残留原子块导致回调不执行的兼容性问题;Redis MQ / RabbitMQ E2E 集成测试全部通过。
864
867
 
865
868
  ### 0.1.5
866
869
 
@@ -57,6 +57,7 @@ django_simpletask5/migrations/0011_taskexecutionstat_canceled_count.py
57
57
  django_simpletask5/migrations/0012_alter_taskexecution_status.py
58
58
  django_simpletask5/migrations/0013_alter_taskexecution_task_id.py
59
59
  django_simpletask5/migrations/0014_cronjob_is_active_modified_by_user.py
60
+ django_simpletask5/migrations/0015_taskexecution_idx_exec_status_created.py
60
61
  django_simpletask5/migrations/__init__.py
61
62
  django_simpletask5/services/__init__.py
62
63
  django_simpletask5/services/archive.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "django-simpletask5"
7
- version = "0.2.9"
7
+ version = "0.2.10"
8
8
  description = "Lightweight async task execution framework for Django"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}