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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/PKG-INFO +20 -1
  2. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/README.md +19 -0
  3. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/core/cronjob_registry.py +2 -1
  4. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/core/defaults.py +1 -0
  5. django_simpletask5-0.2.4/django_simpletask5/core/logging_utils.py +17 -0
  6. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/core/message_queue.py +11 -5
  7. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/core/publisher.py +2 -2
  8. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/retry_timeout.py +5 -5
  9. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/management/commands/django_simpletask_crontab.py +17 -9
  10. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/management/commands/django_simpletask_executor.py +21 -16
  11. django_simpletask5-0.2.4/django_simpletask5/migrations/0010_taskexecution_id_alter_taskexecution_execution_id.py +31 -0
  12. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/models.py +1 -1
  13. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/PKG-INFO +20 -1
  14. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/SOURCES.txt +2 -0
  15. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/pyproject.toml +1 -1
  16. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/LICENSE +0 -0
  17. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/__init__.py +0 -0
  18. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/admin.py +0 -0
  19. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/apps.py +0 -0
  20. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/core/__init__.py +0 -0
  21. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/core/executor_scanner.py +0 -0
  22. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/core/lock.py +0 -0
  23. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/core/signals.py +0 -0
  24. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/core/worker_registry.py +0 -0
  25. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/dashboards.py +0 -0
  26. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/__init__.py +0 -0
  27. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/archive.py +0 -0
  28. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/base.py +0 -0
  29. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/bash_script.py +0 -0
  30. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/exceptions.py +0 -0
  31. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/loader.py +0 -0
  32. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/ping_pong.py +0 -0
  33. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/python_script.py +0 -0
  34. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/simple_request.py +0 -0
  35. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/executors/status_check.py +0 -0
  36. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  37. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.po +0 -0
  38. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/management/__init__.py +0 -0
  39. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/management/commands/__init__.py +0 -0
  40. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/management/commands/django_simpletask_archive.py +0 -0
  41. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/management/commands/django_simpletask_sync_cronjobs.py +0 -0
  42. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0001_initial.py +0 -0
  43. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0002_taskexecution_done_tasks_count_and_more.py +0 -0
  44. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0003_alter_cronjob_options_remove_cronjob_name_and_more.py +0 -0
  45. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0004_alter_cronjob_is_modified_by_user.py +0 -0
  46. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0005_alter_cronjob_is_modified_by_user_help.py +0 -0
  47. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0006_alter_cronjob_options_alter_taskexecution_created_at_and_more.py +0 -0
  48. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0007_rename_task_model_to_executor_class_in_stat.py +0 -0
  49. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0008_taskexecution_pickup_timeout_seconds.py +0 -0
  50. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0009_add_cronjob_app_label.py +0 -0
  51. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/migrations/__init__.py +0 -0
  52. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/services/__init__.py +0 -0
  53. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/services/archive.py +0 -0
  54. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/static/django_simpletask5/css/dashboard.css +0 -0
  55. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5/static/django_simpletask5/js/select2_executor.js +0 -0
  56. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/dependency_links.txt +0 -0
  57. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/requires.txt +0 -0
  58. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/top_level.txt +0 -0
  59. {django_simpletask5-0.2.2 → django_simpletask5-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-simpletask5
3
- Version: 0.2.2
3
+ Version: 0.2.4
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>
@@ -180,6 +180,12 @@ DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS = 3600 # 默认 Pickup 超
180
180
  - **执行超时**(`timeout_seconds`)— 任务进入 `running` 状态后,必须在指定时间内完成,否则被 `StatusCheckExecutor` 标记超时。适用于保护实际执行不 hang 死。
181
181
  - **Pickup 超时**(`pickup_timeout_seconds`)— 任务创建后等待 Worker 消费的时间上限,也用于被拒绝回退后等待重新调度的保护。通常设得比执行超时更长,因为排队等待时间不可控。
182
182
 
183
+ ### 日志
184
+
185
+ ```python
186
+ DJANGO_SIMPLETASK_LOG_INTERVAL = 300 # 死循环存活日志与异常节流日志的输出间隔(秒)
187
+ ```
188
+
183
189
  ### 归档
184
190
 
185
191
  ```python
@@ -368,6 +374,8 @@ python manage.py django_simpletask_sync_cronjobs
368
374
 
369
375
  同步后,用户可以在 Django Admin 中查看和修改 Cron 任务,被手动修改过的任务不会在后续同步中被覆盖(`is_modified_by_user` 标记保护)。
370
376
 
377
+ > 注意:cron 表达式按 Django 的 `TIME_ZONE` 设置对应的本地时间解释。`USE_TZ=True` 时仍以本地时间计算触发时刻(存储为 UTC),例如 `TIME_ZONE = 'Asia/Shanghai'` 下 `30 14 * * *` 表示每天 14:30 本地时间触发。
378
+
371
379
  ## 内置执行器
372
380
 
373
381
  | 执行器 | 说明 |
@@ -392,6 +400,17 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
392
400
 
393
401
  ## Releases
394
402
 
403
+ ### 0.2.4
404
+
405
+ - **破坏性变更**: `TaskExecution` 主键由 `execution_id`(UUID)改为默认自增 `id`,`execution_id` 变为 `unique=True` 的唯一标识字段。已有数据库需要执行迁移 `0010_taskexecution_id_alter_taskexecution_execution_id`
406
+ - **定期存活日志** — Worker 与 Crontab 主循环在长时间无消息/无任务时按 `DJANGO_SIMPLETASK_LOG_INTERVAL`(默认 300 秒)定期输出存活日志,并统一带时间戳格式,便于运维确认进程未死循环
407
+ - **MQ 日志降噪** — MQ 消费/发布异常及 Redis 连接失败、锁续期失败改为 `error`/`warning` 级别且无堆栈(默认按间隔节流),避免刷屏;提供 `--debug` 时输出完整堆栈
408
+ - **新增配置** — `DJANGO_SIMPLETASK_LOG_INTERVAL` 控制死循环存活日志与异常节流日志的输出间隔(秒)
409
+
410
+ ### 0.2.3
411
+
412
+ - **Cron 时区修复** — cron 表达式改为按 Django `TIME_ZONE` 配置的本地时间解释,不再固定按 UTC 计算。`USE_TZ=True` 时使用 `timezone.localtime(now())` 作为 croniter 基准时间,非 UTC 时区下定时任务不再偏移;`USE_TZ=False` 保持原有本地时间行为不变
413
+
395
414
  ### 0.2.2
396
415
 
397
416
  - **抑制异步消息** — 新增 `_skip_publish` 机制,创建/更新/删除 Task 时可通过 `skip_publish()` 方法、`save(skip_publish=True)`、构造时传入 `skip_publish=True` 等方式禁止触发 RabbitMQ 消息通知
@@ -140,6 +140,12 @@ DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS = 3600 # 默认 Pickup 超
140
140
  - **执行超时**(`timeout_seconds`)— 任务进入 `running` 状态后,必须在指定时间内完成,否则被 `StatusCheckExecutor` 标记超时。适用于保护实际执行不 hang 死。
141
141
  - **Pickup 超时**(`pickup_timeout_seconds`)— 任务创建后等待 Worker 消费的时间上限,也用于被拒绝回退后等待重新调度的保护。通常设得比执行超时更长,因为排队等待时间不可控。
142
142
 
143
+ ### 日志
144
+
145
+ ```python
146
+ DJANGO_SIMPLETASK_LOG_INTERVAL = 300 # 死循环存活日志与异常节流日志的输出间隔(秒)
147
+ ```
148
+
143
149
  ### 归档
144
150
 
145
151
  ```python
@@ -328,6 +334,8 @@ python manage.py django_simpletask_sync_cronjobs
328
334
 
329
335
  同步后,用户可以在 Django Admin 中查看和修改 Cron 任务,被手动修改过的任务不会在后续同步中被覆盖(`is_modified_by_user` 标记保护)。
330
336
 
337
+ > 注意:cron 表达式按 Django 的 `TIME_ZONE` 设置对应的本地时间解释。`USE_TZ=True` 时仍以本地时间计算触发时刻(存储为 UTC),例如 `TIME_ZONE = 'Asia/Shanghai'` 下 `30 14 * * *` 表示每天 14:30 本地时间触发。
338
+
331
339
  ## 内置执行器
332
340
 
333
341
  | 执行器 | 说明 |
@@ -352,6 +360,17 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
352
360
 
353
361
  ## Releases
354
362
 
363
+ ### 0.2.4
364
+
365
+ - **破坏性变更**: `TaskExecution` 主键由 `execution_id`(UUID)改为默认自增 `id`,`execution_id` 变为 `unique=True` 的唯一标识字段。已有数据库需要执行迁移 `0010_taskexecution_id_alter_taskexecution_execution_id`
366
+ - **定期存活日志** — Worker 与 Crontab 主循环在长时间无消息/无任务时按 `DJANGO_SIMPLETASK_LOG_INTERVAL`(默认 300 秒)定期输出存活日志,并统一带时间戳格式,便于运维确认进程未死循环
367
+ - **MQ 日志降噪** — MQ 消费/发布异常及 Redis 连接失败、锁续期失败改为 `error`/`warning` 级别且无堆栈(默认按间隔节流),避免刷屏;提供 `--debug` 时输出完整堆栈
368
+ - **新增配置** — `DJANGO_SIMPLETASK_LOG_INTERVAL` 控制死循环存活日志与异常节流日志的输出间隔(秒)
369
+
370
+ ### 0.2.3
371
+
372
+ - **Cron 时区修复** — cron 表达式改为按 Django `TIME_ZONE` 配置的本地时间解释,不再固定按 UTC 计算。`USE_TZ=True` 时使用 `timezone.localtime(now())` 作为 croniter 基准时间,非 UTC 时区下定时任务不再偏移;`USE_TZ=False` 保持原有本地时间行为不变
373
+
355
374
  ### 0.2.2
356
375
 
357
376
  - **抑制异步消息** — 新增 `_skip_publish` 机制,创建/更新/删除 Task 时可通过 `skip_publish()` 方法、`save(skip_publish=True)`、构造时传入 `skip_publish=True` 等方式禁止触发 RabbitMQ 消息通知
@@ -38,7 +38,8 @@ def _compute_next_run_time(cron_expression, base=None):
38
38
  except ImportError:
39
39
  return None
40
40
  if base is None:
41
- base = timezone.now()
41
+ now = timezone.now()
42
+ base = timezone.localtime(now) if timezone.is_aware(now) else now
42
43
  try:
43
44
  cron = croniter(cron_expression, base)
44
45
  return cron.get_next(datetime)
@@ -17,6 +17,7 @@ DJANGO_SIMPLETASK_LOCK_TIMEOUT = getattr(settings, 'DJANGO_SIMPLETASK_LOCK_TIMEO
17
17
  DJANGO_SIMPLETASK_LOCK_RENEW_INTERVAL = getattr(settings, 'DJANGO_SIMPLETASK_LOCK_RENEW_INTERVAL', 60)
18
18
 
19
19
  DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT = getattr(settings, 'DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT', 30)
20
+ DJANGO_SIMPLETASK_LOG_INTERVAL = getattr(settings, 'DJANGO_SIMPLETASK_LOG_INTERVAL', 300)
20
21
  DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_MAX_RETRIES', 3)
21
22
  DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS', 600)
22
23
  DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS = getattr(settings, 'DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS', 3600)
@@ -0,0 +1,17 @@
1
+ import logging
2
+
3
+
4
+ def setup_console_logging(level=logging.INFO):
5
+ """Ensure the root logger logs to console with a timestamped format."""
6
+ root = logging.getLogger()
7
+ root.setLevel(level)
8
+ formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s')
9
+ if not any(isinstance(h, logging.StreamHandler) for h in root.handlers):
10
+ handler = logging.StreamHandler()
11
+ handler.setFormatter(formatter)
12
+ handler.setLevel(level)
13
+ root.addHandler(handler)
14
+ for h in root.handlers:
15
+ if isinstance(h, logging.StreamHandler):
16
+ h.setFormatter(formatter)
17
+ h.setLevel(level)
@@ -5,6 +5,8 @@ import time
5
5
  from django.conf import settings
6
6
  from kombu import Connection, Exchange, Queue, Producer, Consumer
7
7
 
8
+ from django_simpletask5.core.defaults import DJANGO_SIMPLETASK_LOG_INTERVAL
9
+
8
10
  logger = logging.getLogger(__name__)
9
11
 
10
12
 
@@ -66,16 +68,20 @@ class KombuQueue:
66
68
  accept=['json'],
67
69
  ):
68
70
  last_poll = time.time()
71
+ last_heartbeat = time.time()
69
72
  while not self._stopped:
70
73
  try:
71
74
  conn.drain_events(timeout=0.5)
72
75
  except socket.timeout:
73
- pass
76
+ now = time.time()
77
+ if now - last_heartbeat >= DJANGO_SIMPLETASK_LOG_INTERVAL:
78
+ last_heartbeat = now
79
+ logger.info('No message received in the last %s seconds, worker alive', DJANGO_SIMPLETASK_LOG_INTERVAL)
74
80
  if poll_callback and time.time() - last_poll >= poll_interval:
75
81
  try:
76
82
  poll_callback()
77
- except Exception:
78
- logger.exception('Poll callback failed')
83
+ except Exception as e:
84
+ logger.error('Poll callback failed: %s', e)
79
85
  last_poll = time.time()
80
86
 
81
87
  @staticmethod
@@ -85,8 +91,8 @@ class KombuQueue:
85
91
  if result is False:
86
92
  message.reject(requeue=True)
87
93
  return
88
- except Exception:
89
- logger.exception('Error in message callback')
94
+ except Exception as e:
95
+ logger.error('Error in message callback: %s', e)
90
96
  message.reject(requeue=True)
91
97
  return
92
98
  if auto_ack:
@@ -129,12 +129,12 @@ def _publish_message(execution, routing_key=None):
129
129
  )
130
130
  logger.debug('Published message for execution %s', execution.execution_id)
131
131
  return
132
- except Exception:
132
+ except Exception as e:
133
133
  if attempt < 3:
134
134
  logger.warning('Publish attempt %d/3 failed for execution %s, retrying...', attempt, execution.execution_id)
135
135
  time.sleep(1)
136
136
  else:
137
- logger.exception('Failed to publish message for execution %s after 3 attempts', execution.execution_id)
137
+ logger.error('Failed to publish message for execution %s after 3 attempts: %s', execution.execution_id, e)
138
138
 
139
139
  try:
140
140
  execution.refresh_from_db()
@@ -10,7 +10,7 @@ from django_simpletask5.executors.base import BaseExecutor
10
10
  def _publish_message_by_id(execution_id):
11
11
  from django_simpletask5.core.publisher import _publish_message
12
12
  try:
13
- exec_record = TaskExecution.objects.get(pk=execution_id)
13
+ exec_record = TaskExecution.objects.get(execution_id=execution_id)
14
14
  _publish_message(exec_record)
15
15
  except TaskExecution.DoesNotExist:
16
16
  logger.warning('Execution %s not found for retry publish', execution_id)
@@ -31,10 +31,10 @@ class RetryTimeoutExecutor(BaseExecutor):
31
31
  # Pickup timeouts — task was never picked up, don't consume retry
32
32
  pickup_ids = list(
33
33
  timeout_records.filter(started_at__isnull=True)
34
- .values_list('pk', flat=True)
34
+ .values_list('execution_id', flat=True)
35
35
  )
36
36
  if pickup_ids:
37
- TaskExecution.objects.filter(pk__in=pickup_ids).update(
37
+ TaskExecution.objects.filter(execution_id__in=pickup_ids).update(
38
38
  status='retry',
39
39
  )
40
40
  for exec_id in pickup_ids:
@@ -45,10 +45,10 @@ class RetryTimeoutExecutor(BaseExecutor):
45
45
  # Execution timeouts — task was actually running, consume retry
46
46
  exec_ids = list(
47
47
  timeout_records.filter(started_at__isnull=False)
48
- .values_list('pk', flat=True)
48
+ .values_list('execution_id', flat=True)
49
49
  )
50
50
  if exec_ids:
51
- TaskExecution.objects.filter(pk__in=exec_ids).update(
51
+ TaskExecution.objects.filter(execution_id__in=exec_ids).update(
52
52
  status='retry',
53
53
  retry_count=models.F('retry_count') + 1,
54
54
  )
@@ -10,7 +10,8 @@ from django.utils import timezone
10
10
 
11
11
  from django_simpletask5.models import CronJob, TaskExecution
12
12
  from django_simpletask5.core.lock import get_lock_manager
13
- from django_simpletask5.core.defaults import DJANGO_SIMPLETASK_KEY_PREFIX, DJANGO_SIMPLETASK_LOCK_TIMEOUT, DJANGO_SIMPLETASK_DEFAULT_QUEUE, DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS, DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS
13
+ from django_simpletask5.core.defaults import DJANGO_SIMPLETASK_KEY_PREFIX, DJANGO_SIMPLETASK_LOCK_TIMEOUT, DJANGO_SIMPLETASK_DEFAULT_QUEUE, DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS, DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS, DJANGO_SIMPLETASK_LOG_INTERVAL
14
+ from django_simpletask5.core.logging_utils import setup_console_logging
14
15
  from django_simpletask5.executors.loader import load_executor_class
15
16
 
16
17
  logger = logging.getLogger(__name__)
@@ -43,13 +44,15 @@ class Command(BaseCommand):
43
44
  )
44
45
 
45
46
  def handle(self, *args, **options):
46
- global _shutdown
47
+ global _shutdown, _redis_unavailable
48
+ setup_console_logging(logging.INFO)
47
49
  signal.signal(signal.SIGTERM, _signal_handler)
48
50
  signal.signal(signal.SIGINT, _sigint_handler)
49
51
 
50
52
  self.stdout.write('Starting crontab scheduler')
51
53
 
52
54
  lockman = get_lock_manager()
55
+ last_error_log = 0.0
53
56
 
54
57
  while not _shutdown:
55
58
  try:
@@ -60,13 +63,16 @@ class Command(BaseCommand):
60
63
  except Exception as e:
61
64
  close_old_connections()
62
65
  error_str = str(e)
63
- if 'Connection refused' in error_str or 'Error 61' in error_str:
64
- global _redis_unavailable
65
- if not _redis_unavailable:
66
- _redis_unavailable = True
66
+ is_redis_err = 'Connection refused' in error_str or 'Error 61' in error_str
67
+ if is_redis_err:
68
+ _redis_unavailable = True
69
+ now = time.time()
70
+ if now - last_error_log >= DJANGO_SIMPLETASK_LOG_INTERVAL:
71
+ last_error_log = now
72
+ if is_redis_err:
67
73
  logger.warning('Redis connection failed: %s', e)
68
- else:
69
- logger.exception('Error in crontab cycle: %s', e)
74
+ else:
75
+ logger.error('Error in crontab cycle: %s', e)
70
76
  self._sleep_until(timezone.now() + timedelta(seconds=10))
71
77
 
72
78
  self.stdout.write('Crontab scheduler stopped.')
@@ -182,5 +188,7 @@ class Command(BaseCommand):
182
188
  transaction.on_commit(_publish_cronjob)
183
189
 
184
190
  from django_simpletask5.core.cronjob_registry import _compute_next_run_time
185
- cronjob.next_run_time = _compute_next_run_time(cronjob.cron_expression, base=timezone.now())
191
+ now = timezone.now()
192
+ base = timezone.localtime(now) if timezone.is_aware(now) else now
193
+ cronjob.next_run_time = _compute_next_run_time(cronjob.cron_expression, base=base)
186
194
  cronjob.save(update_fields=['next_run_time'])
@@ -19,9 +19,11 @@ from django_simpletask5.core.defaults import (
19
19
  DJANGO_SIMPLETASK_KEY_PREFIX,
20
20
  DJANGO_SIMPLETASK_LOCK_TIMEOUT,
21
21
  DJANGO_SIMPLETASK_SHUTDOWN_TIMEOUT,
22
+ DJANGO_SIMPLETASK_LOG_INTERVAL,
22
23
  DJANGO_SIMPLETASK_DEFAULT_QUEUE,
23
24
  DJANGO_SIMPLETASK_DEFAULT_TIMEOUT_SECONDS,
24
25
  )
26
+ from django_simpletask5.core.logging_utils import setup_console_logging
25
27
  from django_simpletask5.core.worker_registry import (
26
28
  register_worker, update_worker_status, heartbeat_worker, _get_redis_client,
27
29
  )
@@ -59,6 +61,7 @@ _start_lock_renewer._started = False
59
61
 
60
62
  def _lock_renewer_loop():
61
63
  from django_simpletask5.core.defaults import DJANGO_SIMPLETASK_LOCK_RENEW_INTERVAL
64
+ last_log = 0.0
62
65
  while not _shutdown:
63
66
  time.sleep(DJANGO_SIMPLETASK_LOCK_RENEW_INTERVAL)
64
67
  with _active_locks_lock:
@@ -67,7 +70,10 @@ def _lock_renewer_loop():
67
70
  try:
68
71
  lock_ctx.lock.reacquire()
69
72
  except Exception:
70
- logger.debug('Failed to reacquire lock (execution may have completed, or Redis is down)')
73
+ now = time.time()
74
+ if now - last_log >= DJANGO_SIMPLETASK_LOG_INTERVAL:
75
+ last_log = now
76
+ logger.warning('Failed to reacquire lock (execution may have completed, or Redis is down), will keep retrying')
71
77
 
72
78
 
73
79
  def _signal_handler(signum, frame):
@@ -96,6 +102,8 @@ class ExecutorWorker:
96
102
  self.mq = mq
97
103
  self.lock_engine_down = False
98
104
  self._last_heartbeat = 0.0
105
+ self._last_register_log = 0.0
106
+ self._last_consume_error = 0.0
99
107
 
100
108
  def _heartbeat_if_needed(self):
101
109
  now = time.time()
@@ -119,9 +127,11 @@ class ExecutorWorker:
119
127
  logger.info('Redis connection restored')
120
128
  break
121
129
  except Exception:
122
- if not _redis_unavailable:
123
- _redis_unavailable = True
124
- logger.warning('Redis connection failed, retrying in 2s...')
130
+ _redis_unavailable = True
131
+ now = time.time()
132
+ if now - self._last_register_log >= DJANGO_SIMPLETASK_LOG_INTERVAL:
133
+ self._last_register_log = now
134
+ logger.warning('Redis connection failed, retrying every 2s...')
125
135
  time.sleep(2)
126
136
  update_worker_status('idle')
127
137
  logger.info('Worker listening on queue: %s (broker: %s)', self.queue_name, _get_broker_url())
@@ -138,7 +148,10 @@ class ExecutorWorker:
138
148
  except Exception as e:
139
149
  if _shutdown:
140
150
  break
141
- logger.warning('Consumer connection lost (%s), restarting in 1s...', e)
151
+ now = time.time()
152
+ if now - self._last_consume_error >= DJANGO_SIMPLETASK_LOG_INTERVAL:
153
+ self._last_consume_error = now
154
+ logger.error('Consumer connection lost (%s), restarting in 1s...', e)
142
155
  close_old_connections()
143
156
  time.sleep(1)
144
157
 
@@ -300,8 +313,8 @@ class ExecutorWorker:
300
313
  self.mq.publish(routing_key=self.queue_name, message=retry_msg)
301
314
  logger.info('Published retry message for execution %s (attempt %d/%d)',
302
315
  execution.execution_id, execution.retry_count, execution.max_retries)
303
- except Exception:
304
- logger.exception('Failed to publish retry message')
316
+ except Exception as e:
317
+ logger.error('Failed to publish retry message: %s', e)
305
318
 
306
319
 
307
320
  class Command(BaseCommand):
@@ -347,16 +360,8 @@ class Command(BaseCommand):
347
360
  signal.signal(signal.SIGINT, _signal_handler)
348
361
 
349
362
  debug = options['debug']
363
+ setup_console_logging(logging.DEBUG if debug else logging.INFO)
350
364
  if debug:
351
- root = logging.getLogger()
352
- root.setLevel(logging.DEBUG)
353
- if not any(isinstance(h, logging.StreamHandler) for h in root.handlers):
354
- handler = logging.StreamHandler()
355
- handler.setLevel(logging.DEBUG)
356
- handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s'))
357
- root.addHandler(handler)
358
- for h in root.handlers:
359
- h.setLevel(logging.DEBUG)
360
365
  self.stdout.write('Debug logging enabled')
361
366
 
362
367
  num_workers = options['workers']
@@ -0,0 +1,31 @@
1
+ # Generated by Django 5.2.16 on 2026-08-04 19:57
2
+
3
+ import uuid
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("django_simpletask5", "0009_add_cronjob_app_label"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="taskexecution",
16
+ name="id",
17
+ field=models.BigAutoField(
18
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
19
+ ),
20
+ ),
21
+ migrations.AlterField(
22
+ model_name="taskexecution",
23
+ name="execution_id",
24
+ field=models.UUIDField(
25
+ default=uuid.uuid4,
26
+ editable=False,
27
+ unique=True,
28
+ verbose_name="Execution ID",
29
+ ),
30
+ ),
31
+ ]
@@ -152,7 +152,7 @@ class TaskExecution(models.Model):
152
152
  ('timeout', _('Timeout')),
153
153
  ]
154
154
 
155
- execution_id = models.UUIDField(verbose_name=_('Execution ID'), primary_key=True, default=uuid.uuid4, editable=False)
155
+ execution_id = models.UUIDField(verbose_name=_('Execution ID'), unique=True, default=uuid.uuid4, editable=False)
156
156
  task_id = models.UUIDField(verbose_name=_('Task ID'), null=True, blank=True, db_index=True)
157
157
  task_model = models.CharField(verbose_name=_('Task model'), max_length=256, null=True, blank=True, help_text='app_label.ModelName')
158
158
  trigger_event = models.CharField(verbose_name=_('Trigger event'), max_length=64, db_index=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-simpletask5
3
- Version: 0.2.2
3
+ Version: 0.2.4
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>
@@ -180,6 +180,12 @@ DJANGO_SIMPLETASK_DEFAULT_PICKUP_TIMEOUT_SECONDS = 3600 # 默认 Pickup 超
180
180
  - **执行超时**(`timeout_seconds`)— 任务进入 `running` 状态后,必须在指定时间内完成,否则被 `StatusCheckExecutor` 标记超时。适用于保护实际执行不 hang 死。
181
181
  - **Pickup 超时**(`pickup_timeout_seconds`)— 任务创建后等待 Worker 消费的时间上限,也用于被拒绝回退后等待重新调度的保护。通常设得比执行超时更长,因为排队等待时间不可控。
182
182
 
183
+ ### 日志
184
+
185
+ ```python
186
+ DJANGO_SIMPLETASK_LOG_INTERVAL = 300 # 死循环存活日志与异常节流日志的输出间隔(秒)
187
+ ```
188
+
183
189
  ### 归档
184
190
 
185
191
  ```python
@@ -368,6 +374,8 @@ python manage.py django_simpletask_sync_cronjobs
368
374
 
369
375
  同步后,用户可以在 Django Admin 中查看和修改 Cron 任务,被手动修改过的任务不会在后续同步中被覆盖(`is_modified_by_user` 标记保护)。
370
376
 
377
+ > 注意:cron 表达式按 Django 的 `TIME_ZONE` 设置对应的本地时间解释。`USE_TZ=True` 时仍以本地时间计算触发时刻(存储为 UTC),例如 `TIME_ZONE = 'Asia/Shanghai'` 下 `30 14 * * *` 表示每天 14:30 本地时间触发。
378
+
371
379
  ## 内置执行器
372
380
 
373
381
  | 执行器 | 说明 |
@@ -392,6 +400,17 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
392
400
 
393
401
  ## Releases
394
402
 
403
+ ### 0.2.4
404
+
405
+ - **破坏性变更**: `TaskExecution` 主键由 `execution_id`(UUID)改为默认自增 `id`,`execution_id` 变为 `unique=True` 的唯一标识字段。已有数据库需要执行迁移 `0010_taskexecution_id_alter_taskexecution_execution_id`
406
+ - **定期存活日志** — Worker 与 Crontab 主循环在长时间无消息/无任务时按 `DJANGO_SIMPLETASK_LOG_INTERVAL`(默认 300 秒)定期输出存活日志,并统一带时间戳格式,便于运维确认进程未死循环
407
+ - **MQ 日志降噪** — MQ 消费/发布异常及 Redis 连接失败、锁续期失败改为 `error`/`warning` 级别且无堆栈(默认按间隔节流),避免刷屏;提供 `--debug` 时输出完整堆栈
408
+ - **新增配置** — `DJANGO_SIMPLETASK_LOG_INTERVAL` 控制死循环存活日志与异常节流日志的输出间隔(秒)
409
+
410
+ ### 0.2.3
411
+
412
+ - **Cron 时区修复** — cron 表达式改为按 Django `TIME_ZONE` 配置的本地时间解释,不再固定按 UTC 计算。`USE_TZ=True` 时使用 `timezone.localtime(now())` 作为 croniter 基准时间,非 UTC 时区下定时任务不再偏移;`USE_TZ=False` 保持原有本地时间行为不变
413
+
395
414
  ### 0.2.2
396
415
 
397
416
  - **抑制异步消息** — 新增 `_skip_publish` 机制,创建/更新/删除 Task 时可通过 `skip_publish()` 方法、`save(skip_publish=True)`、构造时传入 `skip_publish=True` 等方式禁止触发 RabbitMQ 消息通知
@@ -16,6 +16,7 @@ django_simpletask5/core/cronjob_registry.py
16
16
  django_simpletask5/core/defaults.py
17
17
  django_simpletask5/core/executor_scanner.py
18
18
  django_simpletask5/core/lock.py
19
+ django_simpletask5/core/logging_utils.py
19
20
  django_simpletask5/core/message_queue.py
20
21
  django_simpletask5/core/publisher.py
21
22
  django_simpletask5/core/signals.py
@@ -48,6 +49,7 @@ django_simpletask5/migrations/0006_alter_cronjob_options_alter_taskexecution_cre
48
49
  django_simpletask5/migrations/0007_rename_task_model_to_executor_class_in_stat.py
49
50
  django_simpletask5/migrations/0008_taskexecution_pickup_timeout_seconds.py
50
51
  django_simpletask5/migrations/0009_add_cronjob_app_label.py
52
+ django_simpletask5/migrations/0010_taskexecution_id_alter_taskexecution_execution_id.py
51
53
  django_simpletask5/migrations/__init__.py
52
54
  django_simpletask5/services/__init__.py
53
55
  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.2"
7
+ version = "0.2.4"
8
8
  description = "Lightweight async task execution framework for Django"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}