jettask 0.2.20__py3-none-any.whl → 0.2.24__py3-none-any.whl
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.
- jettask/__init__.py +4 -0
- jettask/cli.py +12 -8
- jettask/config/lua_scripts.py +37 -0
- jettask/config/nacos_config.py +1 -1
- jettask/core/app.py +313 -340
- jettask/core/container.py +4 -4
- jettask/{persistence → core}/namespace.py +93 -27
- jettask/core/task.py +16 -9
- jettask/core/unified_manager_base.py +136 -26
- jettask/db/__init__.py +67 -0
- jettask/db/base.py +137 -0
- jettask/{utils/db_connector.py → db/connector.py} +130 -26
- jettask/db/models/__init__.py +16 -0
- jettask/db/models/scheduled_task.py +196 -0
- jettask/db/models/task.py +77 -0
- jettask/db/models/task_run.py +85 -0
- jettask/executor/__init__.py +0 -15
- jettask/executor/core.py +76 -31
- jettask/executor/process_entry.py +29 -114
- jettask/executor/task_executor.py +4 -0
- jettask/messaging/event_pool.py +928 -685
- jettask/messaging/scanner.py +30 -0
- jettask/persistence/__init__.py +28 -103
- jettask/persistence/buffer.py +170 -0
- jettask/persistence/consumer.py +330 -249
- jettask/persistence/manager.py +304 -0
- jettask/persistence/persistence.py +391 -0
- jettask/scheduler/__init__.py +15 -3
- jettask/scheduler/{task_crud.py → database.py} +61 -57
- jettask/scheduler/loader.py +2 -2
- jettask/scheduler/{scheduler_coordinator.py → manager.py} +23 -6
- jettask/scheduler/models.py +14 -10
- jettask/scheduler/schedule.py +166 -0
- jettask/scheduler/scheduler.py +12 -11
- jettask/schemas/__init__.py +50 -1
- jettask/schemas/backlog.py +43 -6
- jettask/schemas/namespace.py +70 -19
- jettask/schemas/queue.py +19 -3
- jettask/schemas/responses.py +493 -0
- jettask/task/__init__.py +0 -2
- jettask/task/router.py +3 -0
- jettask/test_connection_monitor.py +1 -1
- jettask/utils/__init__.py +7 -5
- jettask/utils/db_init.py +8 -4
- jettask/utils/namespace_dep.py +167 -0
- jettask/utils/queue_matcher.py +186 -0
- jettask/utils/rate_limit/concurrency_limiter.py +7 -1
- jettask/utils/stream_backlog.py +1 -1
- jettask/webui/__init__.py +0 -1
- jettask/webui/api/__init__.py +4 -4
- jettask/webui/api/alerts.py +806 -71
- jettask/webui/api/example_refactored.py +400 -0
- jettask/webui/api/namespaces.py +390 -45
- jettask/webui/api/overview.py +300 -54
- jettask/webui/api/queues.py +971 -267
- jettask/webui/api/scheduled.py +1249 -56
- jettask/webui/api/settings.py +129 -7
- jettask/webui/api/workers.py +442 -0
- jettask/webui/app.py +46 -2329
- jettask/webui/middleware/__init__.py +6 -0
- jettask/webui/middleware/namespace_middleware.py +135 -0
- jettask/webui/services/__init__.py +146 -0
- jettask/webui/services/heartbeat_service.py +251 -0
- jettask/webui/services/overview_service.py +60 -51
- jettask/webui/services/queue_monitor_service.py +426 -0
- jettask/webui/services/redis_monitor_service.py +87 -0
- jettask/webui/services/settings_service.py +174 -111
- jettask/webui/services/task_monitor_service.py +222 -0
- jettask/webui/services/timeline_pg_service.py +452 -0
- jettask/webui/services/timeline_service.py +189 -0
- jettask/webui/services/worker_monitor_service.py +467 -0
- jettask/webui/utils/__init__.py +11 -0
- jettask/webui/utils/time_utils.py +122 -0
- jettask/worker/lifecycle.py +8 -2
- {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/METADATA +1 -1
- jettask-0.2.24.dist-info/RECORD +142 -0
- jettask/executor/executor.py +0 -338
- jettask/persistence/backlog_monitor.py +0 -567
- jettask/persistence/base.py +0 -2334
- jettask/persistence/db_manager.py +0 -516
- jettask/persistence/maintenance.py +0 -81
- jettask/persistence/message_consumer.py +0 -259
- jettask/persistence/models.py +0 -49
- jettask/persistence/offline_recovery.py +0 -196
- jettask/persistence/queue_discovery.py +0 -215
- jettask/persistence/task_persistence.py +0 -218
- jettask/persistence/task_updater.py +0 -583
- jettask/scheduler/add_execution_count.sql +0 -11
- jettask/scheduler/add_priority_field.sql +0 -26
- jettask/scheduler/add_scheduler_id.sql +0 -25
- jettask/scheduler/add_scheduler_id_index.sql +0 -10
- jettask/scheduler/make_scheduler_id_required.sql +0 -28
- jettask/scheduler/migrate_interval_seconds.sql +0 -9
- jettask/scheduler/performance_optimization.sql +0 -45
- jettask/scheduler/run_scheduler.py +0 -186
- jettask/scheduler/schema.sql +0 -84
- jettask/task/task_executor.py +0 -318
- jettask/webui/api/analytics.py +0 -323
- jettask/webui/config.py +0 -90
- jettask/webui/models/__init__.py +0 -3
- jettask/webui/models/namespace.py +0 -63
- jettask/webui/namespace_manager/__init__.py +0 -10
- jettask/webui/namespace_manager/multi.py +0 -593
- jettask/webui/namespace_manager/unified.py +0 -193
- jettask/webui/run.py +0 -46
- jettask-0.2.20.dist-info/RECORD +0 -145
- {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/WHEEL +0 -0
- {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/top_level.txt +0 -0
jettask/scheduler/__init__.py
CHANGED
@@ -1,17 +1,29 @@
|
|
1
1
|
"""
|
2
2
|
定时任务调度模块
|
3
|
-
|
3
|
+
|
4
|
+
文件说明:
|
5
|
+
- models.py: 数据模型(ScheduledTask, TaskExecutionHistory)
|
6
|
+
- schedule.py: Schedule 定义类(用于定义定时任务)
|
7
|
+
- scheduler.py: 核心调度器(TaskScheduler)
|
8
|
+
- loader.py: 任务加载器(TaskLoader)
|
9
|
+
- database.py: 数据库操作(ScheduledTaskManager)
|
10
|
+
- manager.py: 统一调度器管理器(UnifiedSchedulerManager,用于CLI)
|
11
|
+
- sql/: SQL文件目录
|
12
|
+
- schema.sql: 数据库表结构
|
13
|
+
- migrations/: 数据库迁移脚本
|
4
14
|
"""
|
5
15
|
|
6
16
|
from .models import ScheduledTask, TaskExecutionHistory
|
7
17
|
from .scheduler import TaskScheduler
|
8
18
|
from .loader import TaskLoader
|
9
|
-
from .
|
19
|
+
from .database import ScheduledTaskManager
|
20
|
+
from .schedule import Schedule
|
10
21
|
|
11
22
|
__all__ = [
|
12
23
|
'ScheduledTask',
|
13
24
|
'TaskExecutionHistory',
|
14
25
|
'TaskScheduler',
|
15
26
|
'TaskLoader',
|
16
|
-
'ScheduledTaskManager'
|
27
|
+
'ScheduledTaskManager',
|
28
|
+
'Schedule'
|
17
29
|
]
|
@@ -8,6 +8,7 @@ from datetime import datetime, timedelta
|
|
8
8
|
import json
|
9
9
|
|
10
10
|
from .models import ScheduledTask, TaskExecutionHistory, TaskType, TaskStatus
|
11
|
+
from jettask.db.connector import get_asyncpg_pool
|
11
12
|
|
12
13
|
|
13
14
|
class ScheduledTaskManager:
|
@@ -34,14 +35,24 @@ class ScheduledTaskManager:
|
|
34
35
|
|
35
36
|
self.pool: Optional[asyncpg.Pool] = None
|
36
37
|
|
37
|
-
async def connect(self):
|
38
|
-
"""
|
38
|
+
async def connect(self, max_retries: int = 3, retry_delay: int = 5):
|
39
|
+
"""
|
40
|
+
建立数据库连接池(使用统一的连接池管理)
|
41
|
+
|
42
|
+
Args:
|
43
|
+
max_retries: 最大重试次数,默认3次
|
44
|
+
retry_delay: 重试间隔(秒),默认5秒
|
45
|
+
"""
|
39
46
|
if not self.pool:
|
40
|
-
|
41
|
-
|
47
|
+
# 使用统一的 get_asyncpg_pool 函数,它内部已经包含了重试机制和日志
|
48
|
+
self.pool = await get_asyncpg_pool(
|
49
|
+
dsn=self.db_url,
|
42
50
|
min_size=2,
|
43
51
|
max_size=10,
|
44
|
-
command_timeout=60
|
52
|
+
command_timeout=60,
|
53
|
+
timeout=10, # 连接超时10秒
|
54
|
+
max_retries=max_retries,
|
55
|
+
retry_delay=retry_delay
|
45
56
|
)
|
46
57
|
|
47
58
|
async def disconnect(self):
|
@@ -53,7 +64,7 @@ class ScheduledTaskManager:
|
|
53
64
|
async def init_schema(self):
|
54
65
|
"""初始化数据库表结构(幂等操作)"""
|
55
66
|
import os
|
56
|
-
schema_path = os.path.join(os.path.dirname(__file__), 'schema.sql')
|
67
|
+
schema_path = os.path.join(os.path.dirname(__file__), 'sql', 'schema.sql')
|
57
68
|
|
58
69
|
with open(schema_path, 'r') as f:
|
59
70
|
schema_sql = f.read()
|
@@ -76,19 +87,18 @@ class ScheduledTaskManager:
|
|
76
87
|
"""创建定时任务"""
|
77
88
|
sql = """
|
78
89
|
INSERT INTO scheduled_tasks (
|
79
|
-
scheduler_id,
|
90
|
+
scheduler_id, task_type, queue_name, namespace,
|
80
91
|
task_args, task_kwargs, cron_expression, interval_seconds,
|
81
92
|
next_run_time, enabled, max_retries, retry_delay, timeout,
|
82
93
|
priority, description, tags, metadata
|
83
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17
|
94
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
84
95
|
RETURNING *
|
85
96
|
"""
|
86
|
-
|
97
|
+
|
87
98
|
async with self.pool.acquire() as conn:
|
88
99
|
row = await conn.fetchrow(
|
89
100
|
sql,
|
90
101
|
task.scheduler_id,
|
91
|
-
task.task_name,
|
92
102
|
task.task_type.value,
|
93
103
|
task.queue_name,
|
94
104
|
task.namespace, # 添加namespace
|
@@ -106,7 +116,7 @@ class ScheduledTaskManager:
|
|
106
116
|
json.dumps(task.tags),
|
107
117
|
json.dumps(task.metadata)
|
108
118
|
)
|
109
|
-
|
119
|
+
|
110
120
|
return self._row_to_task(row)
|
111
121
|
|
112
122
|
async def get_task(self, task_id: int) -> Optional[ScheduledTask]:
|
@@ -134,34 +144,32 @@ class ScheduledTaskManager:
|
|
134
144
|
sql = """
|
135
145
|
UPDATE scheduled_tasks SET
|
136
146
|
scheduler_id = $2,
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
metadata = $20
|
147
|
+
task_type = $3,
|
148
|
+
queue_name = $4,
|
149
|
+
namespace = $5,
|
150
|
+
task_args = $6,
|
151
|
+
task_kwargs = $7,
|
152
|
+
cron_expression = $8,
|
153
|
+
interval_seconds = $9,
|
154
|
+
next_run_time = $10,
|
155
|
+
last_run_time = $11,
|
156
|
+
enabled = $12,
|
157
|
+
max_retries = $13,
|
158
|
+
retry_delay = $14,
|
159
|
+
timeout = $15,
|
160
|
+
priority = $16,
|
161
|
+
description = $17,
|
162
|
+
tags = $18,
|
163
|
+
metadata = $19
|
155
164
|
WHERE id = $1
|
156
165
|
RETURNING *
|
157
166
|
"""
|
158
|
-
|
167
|
+
|
159
168
|
async with self.pool.acquire() as conn:
|
160
169
|
row = await conn.fetchrow(
|
161
170
|
sql,
|
162
171
|
task.id,
|
163
172
|
task.scheduler_id,
|
164
|
-
task.task_name,
|
165
173
|
task.task_type.value,
|
166
174
|
task.queue_name,
|
167
175
|
task.namespace, # 添加namespace
|
@@ -300,10 +308,9 @@ class ScheduledTaskManager:
|
|
300
308
|
return
|
301
309
|
|
302
310
|
sql = """
|
303
|
-
UPDATE scheduled_tasks
|
304
|
-
SET next_run_time = u.next_run_time,
|
305
|
-
last_run_time = u.last_run_time
|
306
|
-
execution_count = COALESCE(execution_count, 0) + 1
|
311
|
+
UPDATE scheduled_tasks
|
312
|
+
SET next_run_time = u.next_run_time,
|
313
|
+
last_run_time = u.last_run_time
|
307
314
|
FROM (VALUES ($1::int, $2::timestamptz, $3::timestamptz)) AS u(id, next_run_time, last_run_time)
|
308
315
|
WHERE scheduled_tasks.id = u.id
|
309
316
|
"""
|
@@ -439,12 +446,12 @@ class ScheduledTaskManager:
|
|
439
446
|
for history in histories:
|
440
447
|
task_info = task_info_map.get(history.task_id, {})
|
441
448
|
queue_name = task_info.get('queue_name', 'default')
|
442
|
-
task_name
|
443
|
-
|
449
|
+
# task_name 字段已移除,改为 queue_name
|
450
|
+
|
444
451
|
data.append((
|
445
452
|
history.event_id, # id字段使用event_id
|
446
453
|
queue_name,
|
447
|
-
|
454
|
+
queue_name, # task_name(保留以兼容旧表结构,后续可删除)
|
448
455
|
history.status.value if isinstance(history.status, TaskStatus) else history.status,
|
449
456
|
history.task_id, # scheduled_task_id
|
450
457
|
history.scheduled_time,
|
@@ -469,25 +476,24 @@ class ScheduledTaskManager:
|
|
469
476
|
sql = """
|
470
477
|
UPDATE scheduled_tasks SET
|
471
478
|
scheduler_id = $2,
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
updated_at = $18
|
479
|
+
task_type = $3,
|
480
|
+
queue_name = $4,
|
481
|
+
task_args = $5,
|
482
|
+
task_kwargs = $6,
|
483
|
+
cron_expression = $7,
|
484
|
+
interval_seconds = $8,
|
485
|
+
next_run_time = $9,
|
486
|
+
last_run_time = $10,
|
487
|
+
enabled = $11,
|
488
|
+
max_retries = $12,
|
489
|
+
retry_delay = $13,
|
490
|
+
timeout = $14,
|
491
|
+
description = $15,
|
492
|
+
metadata = $16,
|
493
|
+
updated_at = $17
|
488
494
|
WHERE id = $1
|
489
495
|
"""
|
490
|
-
|
496
|
+
|
491
497
|
# 准备批量数据
|
492
498
|
data = []
|
493
499
|
now = datetime.now()
|
@@ -495,7 +501,6 @@ class ScheduledTaskManager:
|
|
495
501
|
data.append((
|
496
502
|
task.id,
|
497
503
|
task.scheduler_id,
|
498
|
-
task.task_name,
|
499
504
|
task.task_type.value if isinstance(task.task_type, TaskType) else task.task_type,
|
500
505
|
task.queue_name,
|
501
506
|
json.dumps(task.task_args) if task.task_args else '[]',
|
@@ -531,7 +536,6 @@ class ScheduledTaskManager:
|
|
531
536
|
return ScheduledTask(
|
532
537
|
id=row['id'],
|
533
538
|
scheduler_id=row['scheduler_id'],
|
534
|
-
task_name=row['task_name'],
|
535
539
|
task_type=TaskType(row['task_type']),
|
536
540
|
queue_name=row['queue_name'],
|
537
541
|
namespace=row.get('namespace', 'default'), # 添加namespace字段
|
jettask/scheduler/loader.py
CHANGED
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
|
|
8
8
|
import json
|
9
9
|
|
10
10
|
from ..utils.task_logger import get_task_logger, LogContext
|
11
|
-
from .
|
11
|
+
from .database import ScheduledTaskManager
|
12
12
|
from .models import ScheduledTask
|
13
13
|
|
14
14
|
|
@@ -55,7 +55,7 @@ class TaskLoader:
|
|
55
55
|
async def connect(self):
|
56
56
|
"""建立Redis连接(使用统一的连接池管理)"""
|
57
57
|
if not self.redis:
|
58
|
-
from jettask.
|
58
|
+
from jettask.db.connector import get_async_redis_client
|
59
59
|
|
60
60
|
self.redis = get_async_redis_client(
|
61
61
|
redis_url=self.redis_url,
|
@@ -10,7 +10,7 @@ from jettask.core.unified_manager_base import UnifiedManagerBase
|
|
10
10
|
from jettask import Jettask
|
11
11
|
from jettask.task.task_center.client import TaskCenter
|
12
12
|
from .scheduler import TaskScheduler
|
13
|
-
from .
|
13
|
+
from .database import ScheduledTaskManager
|
14
14
|
|
15
15
|
logger = logging.getLogger(__name__)
|
16
16
|
|
@@ -50,20 +50,32 @@ class UnifiedSchedulerManager(UnifiedManagerBase):
|
|
50
50
|
async def run_single_namespace(self, namespace_name: str):
|
51
51
|
"""
|
52
52
|
运行单命名空间模式
|
53
|
-
|
53
|
+
|
54
54
|
Args:
|
55
55
|
namespace_name: 命名空间名称
|
56
56
|
"""
|
57
57
|
logger.info(f"启动单命名空间调度器: {namespace_name}")
|
58
58
|
logger.info(f"扫描间隔: {self.scan_interval}秒")
|
59
59
|
logger.info(f"批处理大小: {self.batch_size}")
|
60
|
-
|
60
|
+
|
61
61
|
try:
|
62
|
+
# 构建任务中心URL
|
63
|
+
# 如果URL已经是完整的API格式,直接使用;否则需要转换为标准格式
|
64
|
+
if '/api/v1/namespaces/' in self.task_center_url or '/api/namespaces/' in self.task_center_url:
|
65
|
+
# 已经是完整的API格式
|
66
|
+
namespace_url = self.task_center_url
|
67
|
+
else:
|
68
|
+
# 简化格式,需要转换为标准API格式
|
69
|
+
base_url = self.get_base_url()
|
70
|
+
namespace_url = f"{base_url}/api/v1/namespaces/{namespace_name}"
|
71
|
+
|
72
|
+
logger.debug(f"任务中心API URL: {namespace_url}")
|
73
|
+
|
62
74
|
# 创建任务中心连接
|
63
|
-
tc = TaskCenter(
|
75
|
+
tc = TaskCenter(namespace_url)
|
64
76
|
if not tc._connect_sync():
|
65
|
-
raise Exception(f"无法连接到任务中心: {
|
66
|
-
|
77
|
+
raise Exception(f"无法连接到任务中心: {namespace_url}")
|
78
|
+
|
67
79
|
# 创建 Jettask 应用
|
68
80
|
app = Jettask(task_center=tc)
|
69
81
|
|
@@ -175,6 +187,7 @@ class UnifiedSchedulerManager(UnifiedManagerBase):
|
|
175
187
|
# 1. http://localhost:8001 -> http://localhost:8001/api/v1/namespaces/{name}
|
176
188
|
# 2. http://localhost:8001/api/v1 -> http://localhost:8001/api/v1/namespaces/{name}
|
177
189
|
# 3. http://localhost:8001/api/v1/namespaces/old -> http://localhost:8001/api/v1/namespaces/{name}
|
190
|
+
# 4. http://localhost:8001/namespaces/old -> http://localhost:8001/api/v1/namespaces/{name}
|
178
191
|
|
179
192
|
if '/api/v1/namespaces/' in task_center_url:
|
180
193
|
# 替换现有的命名空间
|
@@ -184,6 +197,10 @@ class UnifiedSchedulerManager(UnifiedManagerBase):
|
|
184
197
|
# 兼容旧格式
|
185
198
|
base_url = task_center_url.split('/api/namespaces/')[0]
|
186
199
|
url = f"{base_url}/api/namespaces/{namespace_name}"
|
200
|
+
elif '/namespaces/' in task_center_url:
|
201
|
+
# 简化格式(无/api前缀),转换为标准格式
|
202
|
+
base_url = task_center_url.split('/namespaces/')[0]
|
203
|
+
url = f"{base_url}/api/v1/namespaces/{namespace_name}"
|
187
204
|
elif task_center_url.endswith('/api/v1') or task_center_url.endswith('/api/v1/'):
|
188
205
|
# 多命名空间模式,URL 是 http://localhost:8001/api/v1
|
189
206
|
url = f"{task_center_url.rstrip('/')}/namespaces/{namespace_name}"
|
jettask/scheduler/models.py
CHANGED
@@ -29,11 +29,15 @@ class TaskStatus(Enum):
|
|
29
29
|
|
30
30
|
@dataclass
|
31
31
|
class ScheduledTask:
|
32
|
-
"""
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
"""
|
33
|
+
定时任务模型
|
34
|
+
|
35
|
+
定时任务以 queue 为核心,定期向指定队列发送消息。
|
36
|
+
队列中的消息可以被多个消费者处理,具体处理逻辑由消费者决定。
|
37
|
+
"""
|
38
|
+
task_type: TaskType # 任务类型(once/interval/cron)
|
39
|
+
queue_name: str # 目标队列(必填)
|
40
|
+
|
37
41
|
# 可选字段
|
38
42
|
id: Optional[int] = None # 数据库自增ID(唯一标识)
|
39
43
|
scheduler_id: Optional[str] = None # 任务的唯一标识符(用于去重)
|
@@ -71,14 +75,14 @@ class ScheduledTask:
|
|
71
75
|
def _validate(self):
|
72
76
|
"""验证任务配置"""
|
73
77
|
if self.task_type == TaskType.CRON and not self.cron_expression:
|
74
|
-
raise ValueError(f"Task {self.
|
75
|
-
|
78
|
+
raise ValueError(f"Task (queue={self.queue_name}) with type CRON must have cron_expression")
|
79
|
+
|
76
80
|
if self.task_type == TaskType.INTERVAL and not self.interval_seconds:
|
77
|
-
raise ValueError(f"Task {self.
|
78
|
-
|
81
|
+
raise ValueError(f"Task (queue={self.queue_name}) with type INTERVAL must have interval_seconds")
|
82
|
+
|
79
83
|
# ONCE类型任务不应该有interval_seconds参数
|
80
84
|
if self.task_type == TaskType.ONCE and self.interval_seconds is not None:
|
81
|
-
raise ValueError(f"Task {self.
|
85
|
+
raise ValueError(f"Task (queue={self.queue_name}) with type ONCE should not have interval_seconds. Use next_run_time to specify when to run the task")
|
82
86
|
|
83
87
|
def calculate_next_run_time(self, from_time: Optional[datetime] = None) -> Optional[datetime]:
|
84
88
|
"""计算下次执行时间"""
|
@@ -0,0 +1,166 @@
|
|
1
|
+
"""
|
2
|
+
定时任务定义类 - 类似 TaskMessage 的设计模式
|
3
|
+
用于定义定时任务,然后统一注册到数据库
|
4
|
+
"""
|
5
|
+
from typing import Optional, Dict, Any, List
|
6
|
+
from dataclasses import dataclass, field
|
7
|
+
from datetime import datetime, timedelta
|
8
|
+
from .models import TaskType
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class Schedule:
|
13
|
+
"""
|
14
|
+
定时任务定义对象
|
15
|
+
|
16
|
+
这是一个定义定时任务的类,类似 TaskMessage 的设计模式。
|
17
|
+
定时任务会定期向指定队列发送消息,队列中的消息可以被多个消费者处理。
|
18
|
+
|
19
|
+
使用示例:
|
20
|
+
# 1. 定义定时任务
|
21
|
+
schedule1 = Schedule(
|
22
|
+
scheduler_id="notify_every_30s",
|
23
|
+
queue="notification_queue",
|
24
|
+
interval_seconds=30,
|
25
|
+
kwargs={"user_id": "user_123", "message": "定时提醒"}
|
26
|
+
)
|
27
|
+
|
28
|
+
schedule2 = Schedule(
|
29
|
+
scheduler_id="report_cron",
|
30
|
+
queue="report_queue",
|
31
|
+
cron_expression="0 9 * * *", # 每天9点
|
32
|
+
description="每天生成报告"
|
33
|
+
)
|
34
|
+
|
35
|
+
# 2. 批量注册
|
36
|
+
await app.register_schedules([schedule1, schedule2])
|
37
|
+
|
38
|
+
# 3. 或单个注册
|
39
|
+
await app.register_schedules(schedule1)
|
40
|
+
"""
|
41
|
+
|
42
|
+
# 必需参数
|
43
|
+
scheduler_id: str # 任务唯一标识符(必填)
|
44
|
+
queue: str # 目标队列(必填)
|
45
|
+
|
46
|
+
# 调度类型(三选一)
|
47
|
+
interval_seconds: Optional[float] = None # 间隔任务(秒)
|
48
|
+
cron_expression: Optional[str] = None # Cron表达式任务
|
49
|
+
next_run_time: Optional[datetime] = None # 一次性任务(指定执行时间)
|
50
|
+
|
51
|
+
# 任务参数
|
52
|
+
args: List[Any] = field(default_factory=list)
|
53
|
+
kwargs: Dict[str, Any] = field(default_factory=dict)
|
54
|
+
|
55
|
+
# 执行选项
|
56
|
+
priority: Optional[int] = None # 优先级(1最高,数字越大优先级越低)
|
57
|
+
timeout: int = 300 # 超时时间(秒)
|
58
|
+
max_retries: int = 3 # 最大重试次数
|
59
|
+
retry_delay: int = 60 # 重试延迟(秒)
|
60
|
+
|
61
|
+
# 控制选项
|
62
|
+
enabled: bool = True # 是否启用
|
63
|
+
description: Optional[str] = None # 任务描述
|
64
|
+
tags: List[str] = field(default_factory=list) # 标签
|
65
|
+
metadata: Dict[str, Any] = field(default_factory=dict) # 额外元数据
|
66
|
+
|
67
|
+
# 注册选项
|
68
|
+
skip_if_exists: bool = True # 如果已存在则跳过
|
69
|
+
at_once: bool = True # 是否立即执行一次
|
70
|
+
|
71
|
+
def __post_init__(self):
|
72
|
+
"""初始化后处理"""
|
73
|
+
self._validate()
|
74
|
+
|
75
|
+
def _validate(self):
|
76
|
+
"""验证配置"""
|
77
|
+
if not self.scheduler_id:
|
78
|
+
raise ValueError("scheduler_id is required")
|
79
|
+
|
80
|
+
if not self.queue:
|
81
|
+
raise ValueError("queue is required")
|
82
|
+
|
83
|
+
# 检查调度类型(必须指定一种)
|
84
|
+
schedule_types = [
|
85
|
+
self.interval_seconds is not None,
|
86
|
+
self.cron_expression is not None,
|
87
|
+
self.next_run_time is not None
|
88
|
+
]
|
89
|
+
|
90
|
+
if sum(schedule_types) == 0:
|
91
|
+
raise ValueError(
|
92
|
+
"Must specify one schedule type: "
|
93
|
+
"interval_seconds, cron_expression, or next_run_time"
|
94
|
+
)
|
95
|
+
|
96
|
+
if sum(schedule_types) > 1:
|
97
|
+
raise ValueError(
|
98
|
+
"Can only specify one schedule type: "
|
99
|
+
"interval_seconds, cron_expression, or next_run_time"
|
100
|
+
)
|
101
|
+
|
102
|
+
# 验证具体参数
|
103
|
+
if self.interval_seconds is not None and self.interval_seconds <= 0:
|
104
|
+
raise ValueError(f"interval_seconds must be positive, got {self.interval_seconds}")
|
105
|
+
|
106
|
+
if self.priority is not None and self.priority < 1:
|
107
|
+
raise ValueError(f"priority must be positive (1 is highest), got {self.priority}")
|
108
|
+
|
109
|
+
def get_task_type(self) -> TaskType:
|
110
|
+
"""获取任务类型"""
|
111
|
+
if self.interval_seconds is not None:
|
112
|
+
return TaskType.INTERVAL
|
113
|
+
elif self.cron_expression is not None:
|
114
|
+
return TaskType.CRON
|
115
|
+
else:
|
116
|
+
return TaskType.ONCE
|
117
|
+
|
118
|
+
def to_dict(self) -> dict:
|
119
|
+
"""
|
120
|
+
转换为字典格式(用于注册到数据库)
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
dict: 任务字典
|
124
|
+
"""
|
125
|
+
data = {
|
126
|
+
'scheduler_id': self.scheduler_id,
|
127
|
+
'queue': self.queue,
|
128
|
+
'task_type': self.get_task_type().value,
|
129
|
+
'task_args': self.args,
|
130
|
+
'task_kwargs': self.kwargs,
|
131
|
+
'enabled': self.enabled,
|
132
|
+
'priority': self.priority,
|
133
|
+
'timeout': self.timeout,
|
134
|
+
'max_retries': self.max_retries,
|
135
|
+
'retry_delay': self.retry_delay,
|
136
|
+
'description': self.description,
|
137
|
+
'tags': self.tags,
|
138
|
+
'metadata': self.metadata,
|
139
|
+
}
|
140
|
+
|
141
|
+
# 添加调度相关参数
|
142
|
+
if self.interval_seconds is not None:
|
143
|
+
data['interval_seconds'] = self.interval_seconds
|
144
|
+
elif self.cron_expression is not None:
|
145
|
+
data['cron_expression'] = self.cron_expression
|
146
|
+
elif self.next_run_time is not None:
|
147
|
+
data['next_run_time'] = self.next_run_time
|
148
|
+
|
149
|
+
return data
|
150
|
+
|
151
|
+
def __repr__(self) -> str:
|
152
|
+
"""友好的字符串表示"""
|
153
|
+
parts = [f"Schedule(scheduler_id='{self.scheduler_id}'"]
|
154
|
+
parts.append(f"queue='{self.queue}'")
|
155
|
+
|
156
|
+
if self.interval_seconds:
|
157
|
+
parts.append(f"interval={self.interval_seconds}s")
|
158
|
+
elif self.cron_expression:
|
159
|
+
parts.append(f"cron='{self.cron_expression}'")
|
160
|
+
elif self.next_run_time:
|
161
|
+
parts.append(f"at={self.next_run_time}")
|
162
|
+
|
163
|
+
if self.kwargs:
|
164
|
+
parts.append(f"kwargs={self.kwargs}")
|
165
|
+
|
166
|
+
return ", ".join(parts) + ")"
|
jettask/scheduler/scheduler.py
CHANGED
@@ -10,7 +10,7 @@ from typing import Optional, List, TYPE_CHECKING
|
|
10
10
|
from datetime import datetime
|
11
11
|
|
12
12
|
from ..utils.task_logger import get_task_logger, LogContext
|
13
|
-
from .
|
13
|
+
from .database import ScheduledTaskManager
|
14
14
|
from .models import ScheduledTask, TaskExecutionHistory, TaskType, TaskStatus as ScheduledTaskStatus
|
15
15
|
from .loader import TaskLoader
|
16
16
|
|
@@ -499,12 +499,12 @@ class TaskScheduler:
|
|
499
499
|
from ..core.message import TaskMessage
|
500
500
|
task_messages = []
|
501
501
|
for task in bulk_tasks:
|
502
|
-
# 准备kwargs
|
503
|
-
kwargs = task.task_kwargs
|
504
|
-
|
505
|
-
|
502
|
+
# 准备kwargs(合并task定义的kwargs)
|
503
|
+
kwargs = task.task_kwargs.copy() if task.task_kwargs else {}
|
504
|
+
|
505
|
+
# 添加scheduled_task_id用于跟踪
|
506
506
|
kwargs['__scheduled_task_id'] = task.id
|
507
|
-
|
507
|
+
|
508
508
|
# 将timeout、max_retries等参数放入kwargs中传递
|
509
509
|
if task.timeout:
|
510
510
|
kwargs['__timeout'] = task.timeout
|
@@ -512,17 +512,18 @@ class TaskScheduler:
|
|
512
512
|
kwargs['__max_retries'] = task.max_retries
|
513
513
|
if task.retry_delay:
|
514
514
|
kwargs['__retry_delay'] = task.retry_delay
|
515
|
-
|
516
|
-
# 创建TaskMessage
|
515
|
+
|
516
|
+
# 创建TaskMessage(直接发送到queue,由queue的消费者处理)
|
517
517
|
task_msg = TaskMessage(
|
518
518
|
queue=task.queue_name,
|
519
|
+
args=task.task_args or [],
|
519
520
|
kwargs=kwargs,
|
520
521
|
priority=task.priority
|
521
522
|
)
|
522
523
|
task_messages.append(task_msg)
|
523
524
|
|
524
|
-
# 使用send_tasks
|
525
|
-
event_ids = await self.app.send_tasks(task_messages)
|
525
|
+
# 使用send_tasks方法发送(异步模式)
|
526
|
+
event_ids = await self.app.send_tasks(task_messages, asyncio=True)
|
526
527
|
logger.info(f"Triggered {len(task_messages)} tasks via send_tasks")
|
527
528
|
|
528
529
|
# 准备批量操作的数据
|
@@ -620,7 +621,7 @@ class TaskScheduler:
|
|
620
621
|
"""运行调度器主循环"""
|
621
622
|
# 建立Redis连接(使用统一的连接池管理)
|
622
623
|
if not self.redis:
|
623
|
-
from jettask.
|
624
|
+
from jettask.db.connector import get_async_redis_client
|
624
625
|
|
625
626
|
self.redis = get_async_redis_client(
|
626
627
|
redis_url=self.redis_url,
|
jettask/schemas/__init__.py
CHANGED
@@ -57,6 +57,7 @@ from .namespace import (
|
|
57
57
|
|
58
58
|
# 积压监控相关模型
|
59
59
|
from .backlog import (
|
60
|
+
BacklogLatestRequest,
|
60
61
|
BacklogTrendRequest,
|
61
62
|
BacklogSnapshot,
|
62
63
|
BacklogStatistics,
|
@@ -91,6 +92,31 @@ from .monitoring import (
|
|
91
92
|
WorkerMetrics
|
92
93
|
)
|
93
94
|
|
95
|
+
# API 响应模型
|
96
|
+
from .responses import (
|
97
|
+
SuccessResponse,
|
98
|
+
DataResponse,
|
99
|
+
PaginatedDataResponse,
|
100
|
+
SystemStatsData,
|
101
|
+
SystemStatsResponse,
|
102
|
+
DashboardStatsData,
|
103
|
+
DashboardStatsResponse,
|
104
|
+
QueueRankingItem,
|
105
|
+
TopQueuesResponse,
|
106
|
+
NamespaceStatistics,
|
107
|
+
NamespaceStatisticsResponse,
|
108
|
+
WorkerHeartbeatInfo,
|
109
|
+
WorkersResponse,
|
110
|
+
WorkerSummary,
|
111
|
+
WorkerSummaryResponse,
|
112
|
+
WorkerOfflineRecord,
|
113
|
+
WorkerOfflineHistoryResponse,
|
114
|
+
DatabaseStatus,
|
115
|
+
SystemSettings,
|
116
|
+
SystemSettingsResponse,
|
117
|
+
DatabaseStatusResponse
|
118
|
+
)
|
119
|
+
|
94
120
|
__all__ = [
|
95
121
|
# Task
|
96
122
|
'TasksRequest',
|
@@ -162,5 +188,28 @@ __all__ = [
|
|
162
188
|
'SystemHealth',
|
163
189
|
'DashboardOverviewRequest',
|
164
190
|
'DashboardOverview',
|
165
|
-
'WorkerMetrics'
|
191
|
+
'WorkerMetrics',
|
192
|
+
|
193
|
+
# Responses
|
194
|
+
'SuccessResponse',
|
195
|
+
'DataResponse',
|
196
|
+
'PaginatedDataResponse',
|
197
|
+
'SystemStatsData',
|
198
|
+
'SystemStatsResponse',
|
199
|
+
'DashboardStatsData',
|
200
|
+
'DashboardStatsResponse',
|
201
|
+
'QueueRankingItem',
|
202
|
+
'TopQueuesResponse',
|
203
|
+
'NamespaceStatistics',
|
204
|
+
'NamespaceStatisticsResponse',
|
205
|
+
'WorkerHeartbeatInfo',
|
206
|
+
'WorkersResponse',
|
207
|
+
'WorkerSummary',
|
208
|
+
'WorkerSummaryResponse',
|
209
|
+
'WorkerOfflineRecord',
|
210
|
+
'WorkerOfflineHistoryResponse',
|
211
|
+
'DatabaseStatus',
|
212
|
+
'SystemSettings',
|
213
|
+
'SystemSettingsResponse',
|
214
|
+
'DatabaseStatusResponse'
|
166
215
|
]
|