django-simpletask5 0.2.3__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.
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/PKG-INFO +14 -1
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/README.md +13 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/defaults.py +1 -0
- django_simpletask5-0.2.4/django_simpletask5/core/logging_utils.py +17 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/message_queue.py +11 -5
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/publisher.py +2 -2
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/retry_timeout.py +5 -5
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/management/commands/django_simpletask_crontab.py +14 -8
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/management/commands/django_simpletask_executor.py +21 -16
- django_simpletask5-0.2.4/django_simpletask5/migrations/0010_taskexecution_id_alter_taskexecution_execution_id.py +31 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/models.py +1 -1
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/PKG-INFO +14 -1
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/SOURCES.txt +2 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/pyproject.toml +1 -1
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/LICENSE +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/__init__.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/admin.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/apps.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/__init__.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/cronjob_registry.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/executor_scanner.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/lock.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/signals.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/worker_registry.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/dashboards.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/__init__.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/archive.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/base.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/bash_script.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/exceptions.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/loader.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/ping_pong.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/python_script.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/simple_request.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/status_check.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/locale/zh_Hans/LC_MESSAGES/django.po +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/management/__init__.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/management/commands/__init__.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/management/commands/django_simpletask_archive.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/management/commands/django_simpletask_sync_cronjobs.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0001_initial.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0002_taskexecution_done_tasks_count_and_more.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0003_alter_cronjob_options_remove_cronjob_name_and_more.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0004_alter_cronjob_is_modified_by_user.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0005_alter_cronjob_is_modified_by_user_help.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0006_alter_cronjob_options_alter_taskexecution_created_at_and_more.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0007_rename_task_model_to_executor_class_in_stat.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0008_taskexecution_pickup_timeout_seconds.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0009_add_cronjob_app_label.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/__init__.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/services/__init__.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/services/archive.py +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/static/django_simpletask5/css/dashboard.css +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/static/django_simpletask5/js/select2_executor.js +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/dependency_links.txt +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/requires.txt +0 -0
- {django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/top_level.txt +0 -0
- {django_simpletask5-0.2.3 → 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.
|
|
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
|
|
@@ -394,6 +400,13 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
394
400
|
|
|
395
401
|
## Releases
|
|
396
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
|
+
|
|
397
410
|
### 0.2.3
|
|
398
411
|
|
|
399
412
|
- **Cron 时区修复** — cron 表达式改为按 Django `TIME_ZONE` 配置的本地时间解释,不再固定按 UTC 计算。`USE_TZ=True` 时使用 `timezone.localtime(now())` 作为 croniter 基准时间,非 UTC 时区下定时任务不再偏移;`USE_TZ=False` 保持原有本地时间行为不变
|
|
@@ -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
|
|
@@ -354,6 +360,13 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
354
360
|
|
|
355
361
|
## Releases
|
|
356
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
|
+
|
|
357
370
|
### 0.2.3
|
|
358
371
|
|
|
359
372
|
- **Cron 时区修复** — cron 表达式改为按 Django `TIME_ZONE` 配置的本地时间解释,不再固定按 UTC 计算。`USE_TZ=True` 时使用 `timezone.localtime(now())` 作为 croniter 基准时间,非 UTC 时区下定时任务不再偏移;`USE_TZ=False` 保持原有本地时间行为不变
|
|
@@ -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)
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/message_queue.py
RENAMED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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()
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/retry_timeout.py
RENAMED
|
@@ -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(
|
|
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('
|
|
34
|
+
.values_list('execution_id', flat=True)
|
|
35
35
|
)
|
|
36
36
|
if pickup_ids:
|
|
37
|
-
TaskExecution.objects.filter(
|
|
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('
|
|
48
|
+
.values_list('execution_id', flat=True)
|
|
49
49
|
)
|
|
50
50
|
if exec_ids:
|
|
51
|
-
TaskExecution.objects.filter(
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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.')
|
|
@@ -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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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.
|
|
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'),
|
|
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.
|
|
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
|
|
@@ -394,6 +400,13 @@ Worker 消费消息 → 获取分布式锁 → 加载执行器 → 执行并保
|
|
|
394
400
|
|
|
395
401
|
## Releases
|
|
396
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
|
+
|
|
397
410
|
### 0.2.3
|
|
398
411
|
|
|
399
412
|
- **Cron 时区修复** — cron 表达式改为按 Django `TIME_ZONE` 配置的本地时间解释,不再固定按 UTC 计算。`USE_TZ=True` 时使用 `timezone.localtime(now())` 作为 croniter 基准时间,非 UTC 时区下定时任务不再偏移;`USE_TZ=False` 保持原有本地时间行为不变
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/cronjob_registry.py
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/executor_scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/core/worker_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/__init__.py
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/archive.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/bash_script.py
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/exceptions.py
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/loader.py
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/ping_pong.py
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/python_script.py
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/simple_request.py
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/executors/status_check.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/management/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/migrations/__init__.py
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/services/__init__.py
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5/services/archive.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/requires.txt
RENAMED
|
File without changes
|
{django_simpletask5-0.2.3 → django_simpletask5-0.2.4}/django_simpletask5.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|