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
@@ -1,28 +0,0 @@
|
|
1
|
-
-- Migration to make scheduler_id NOT NULL
|
2
|
-
-- First, update any NULL scheduler_id values with generated ones
|
3
|
-
|
4
|
-
-- Update NULL scheduler_id values with generated unique IDs
|
5
|
-
UPDATE scheduled_tasks
|
6
|
-
SET scheduler_id = CONCAT(task_name, '_', task_type, '_', id)
|
7
|
-
WHERE scheduler_id IS NULL;
|
8
|
-
|
9
|
-
-- Now make the column NOT NULL
|
10
|
-
ALTER TABLE scheduled_tasks
|
11
|
-
ALTER COLUMN scheduler_id SET NOT NULL;
|
12
|
-
|
13
|
-
-- Ensure the UNIQUE constraint exists
|
14
|
-
DO $$
|
15
|
-
BEGIN
|
16
|
-
IF NOT EXISTS (
|
17
|
-
SELECT 1
|
18
|
-
FROM pg_constraint
|
19
|
-
WHERE conname = 'scheduled_tasks_scheduler_id_key'
|
20
|
-
) THEN
|
21
|
-
ALTER TABLE scheduled_tasks
|
22
|
-
ADD CONSTRAINT scheduled_tasks_scheduler_id_key UNIQUE (scheduler_id);
|
23
|
-
END IF;
|
24
|
-
END $$;
|
25
|
-
|
26
|
-
-- Update the comment
|
27
|
-
COMMENT ON COLUMN scheduled_tasks.scheduler_id IS
|
28
|
-
'Unique identifier for the task (required, used for deduplication)';
|
@@ -1,9 +0,0 @@
|
|
1
|
-
-- Migration to change interval_seconds from INTEGER to NUMERIC
|
2
|
-
-- This allows storing decimal values like 0.1 seconds
|
3
|
-
|
4
|
-
-- Alter the column type
|
5
|
-
ALTER TABLE scheduled_tasks
|
6
|
-
ALTER COLUMN interval_seconds TYPE NUMERIC(10,2);
|
7
|
-
|
8
|
-
-- Add a comment to document the change
|
9
|
-
COMMENT ON COLUMN scheduled_tasks.interval_seconds IS 'Interval in seconds for interval-type tasks (supports decimal values)';
|
@@ -1,45 +0,0 @@
|
|
1
|
-
-- Performance optimization for scheduled_tasks table
|
2
|
-
-- This migration adds necessary indexes for optimal query performance
|
3
|
-
|
4
|
-
-- 1. Unique index on scheduler_id (已经存在,但确保创建)
|
5
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_scheduled_tasks_scheduler_id
|
6
|
-
ON scheduled_tasks(scheduler_id);
|
7
|
-
|
8
|
-
-- 2. Index for ready tasks query (get_ready_tasks)
|
9
|
-
-- 这是最频繁的查询之一,需要复合索引
|
10
|
-
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_ready
|
11
|
-
ON scheduled_tasks(next_run_time, enabled)
|
12
|
-
WHERE enabled = true AND next_run_time IS NOT NULL;
|
13
|
-
|
14
|
-
-- 3. Index for task listing with filters (list_tasks)
|
15
|
-
-- created_at用于排序
|
16
|
-
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_created
|
17
|
-
ON scheduled_tasks(created_at DESC);
|
18
|
-
|
19
|
-
-- 4. Index for task_name lookups (常用于查找特定任务)
|
20
|
-
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_name
|
21
|
-
ON scheduled_tasks(task_name);
|
22
|
-
|
23
|
-
-- 5. Composite index for enabled tasks with type
|
24
|
-
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_enabled_type
|
25
|
-
ON scheduled_tasks(enabled, task_type)
|
26
|
-
WHERE enabled = true;
|
27
|
-
|
28
|
-
-- 6. Index for update operations by id (primary key已自动有索引)
|
29
|
-
-- 但确保主键约束存在
|
30
|
-
-- (主键自动创建索引,无需额外操作)
|
31
|
-
|
32
|
-
-- 7. Index for task execution history queries
|
33
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_task_scheduled
|
34
|
-
ON task_execution_history(task_id, scheduled_time DESC);
|
35
|
-
|
36
|
-
-- 8. Index for history status queries
|
37
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_status_created
|
38
|
-
ON task_execution_history(status, created_at DESC);
|
39
|
-
|
40
|
-
-- 添加表注释
|
41
|
-
COMMENT ON TABLE scheduled_tasks IS 'Scheduled tasks configuration table with optimized indexes';
|
42
|
-
|
43
|
-
-- 分析表以更新统计信息
|
44
|
-
ANALYZE scheduled_tasks;
|
45
|
-
ANALYZE task_execution_history;
|
@@ -1,186 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python
|
2
|
-
"""
|
3
|
-
定时任务调度器启动脚本
|
4
|
-
"""
|
5
|
-
import asyncio
|
6
|
-
import os
|
7
|
-
import sys
|
8
|
-
import signal
|
9
|
-
from pathlib import Path
|
10
|
-
|
11
|
-
# 添加项目根目录到Python路径
|
12
|
-
project_root = Path(__file__).parent.parent.parent
|
13
|
-
sys.path.insert(0, str(project_root))
|
14
|
-
|
15
|
-
from jettask import Jettask
|
16
|
-
from jettask.scheduler.scheduler import TaskScheduler
|
17
|
-
from jettask.scheduler.task_crud import ScheduledTaskManager
|
18
|
-
from jettask.utils.task_logger import get_task_logger
|
19
|
-
|
20
|
-
logger = get_task_logger(__name__)
|
21
|
-
|
22
|
-
|
23
|
-
class SchedulerRunner:
|
24
|
-
"""调度器运行器"""
|
25
|
-
|
26
|
-
def __init__(self):
|
27
|
-
self.scheduler = None
|
28
|
-
self.app = None
|
29
|
-
self.running = False
|
30
|
-
|
31
|
-
async def setup(self):
|
32
|
-
"""初始化设置"""
|
33
|
-
# 从环境变量获取配置
|
34
|
-
redis_url = os.getenv('JETTASK_REDIS_URL', 'redis://localhost:6379/0')
|
35
|
-
db_url = os.getenv('JETTASK_PG_URL', 'postgresql://jettask:123456@localhost:5432/jettask')
|
36
|
-
|
37
|
-
# 创建Jettask应用实例
|
38
|
-
self.app = Jettask(
|
39
|
-
redis_url=redis_url,
|
40
|
-
pg_url=db_url
|
41
|
-
)
|
42
|
-
|
43
|
-
# 导入任务模块(确保任务被注册)
|
44
|
-
try:
|
45
|
-
# 尝试导入用户定义的任务模块
|
46
|
-
import importlib
|
47
|
-
import importlib.util
|
48
|
-
|
49
|
-
# 先尝试加载示例模块
|
50
|
-
example_paths = [
|
51
|
-
'examples.scheduler_auto_demo',
|
52
|
-
'scheduler_auto_demo'
|
53
|
-
]
|
54
|
-
|
55
|
-
for path in example_paths:
|
56
|
-
try:
|
57
|
-
example_module = importlib.import_module(path)
|
58
|
-
if hasattr(example_module, 'app'):
|
59
|
-
# 将任务模块中的任务复制到调度器的app中
|
60
|
-
for task_name, task_obj in example_module.app._tasks.items():
|
61
|
-
self.app._tasks[task_name] = task_obj
|
62
|
-
# 也注册带模块名的任务
|
63
|
-
full_name = f"scheduler_auto_demo.{task_name}"
|
64
|
-
self.app._tasks[full_name] = task_obj
|
65
|
-
logger.info(f"Registered task: {task_name} and {full_name}")
|
66
|
-
logger.info(f"Loaded example tasks from module: {path}")
|
67
|
-
break
|
68
|
-
except ImportError:
|
69
|
-
continue
|
70
|
-
|
71
|
-
# 然后加载用户指定的任务模块
|
72
|
-
task_module = os.getenv('JETTASK_TASKS_MODULE', 'tasks')
|
73
|
-
if task_module:
|
74
|
-
try:
|
75
|
-
module = importlib.import_module(task_module)
|
76
|
-
# 获取模块中的app对象的任务
|
77
|
-
if hasattr(module, 'app'):
|
78
|
-
# 将任务模块中的任务复制到调度器的app中
|
79
|
-
for task_name, task_obj in module.app._tasks.items():
|
80
|
-
self.app._tasks[task_name] = task_obj
|
81
|
-
logger.info(f"Registered task: {task_name}")
|
82
|
-
|
83
|
-
# 注册别名任务(scheduler_auto_demo.send_notification)
|
84
|
-
if hasattr(module, 'send_notification'):
|
85
|
-
self.app._tasks['scheduler_auto_demo.send_notification'] = module.send_notification
|
86
|
-
logger.info("Registered task: scheduler_auto_demo.send_notification")
|
87
|
-
|
88
|
-
logger.info(f"Loaded tasks from module: {task_module}")
|
89
|
-
except ImportError as e:
|
90
|
-
logger.warning(f"Could not import tasks module '{task_module}': {e}")
|
91
|
-
except Exception as e:
|
92
|
-
logger.warning(f"Error loading tasks: {e}")
|
93
|
-
|
94
|
-
# 创建数据库管理器
|
95
|
-
db_manager = ScheduledTaskManager(db_url)
|
96
|
-
await db_manager.connect()
|
97
|
-
|
98
|
-
# 确保数据库表存在
|
99
|
-
await db_manager.init_schema()
|
100
|
-
|
101
|
-
# 创建调度器
|
102
|
-
self.scheduler = TaskScheduler(
|
103
|
-
app=self.app,
|
104
|
-
redis_url=redis_url,
|
105
|
-
db_manager=db_manager,
|
106
|
-
redis_prefix="jettask:scheduled",
|
107
|
-
scan_interval=0.5, # 每0.5秒扫描一次
|
108
|
-
batch_size=100, # 每批处理100个任务
|
109
|
-
leader_ttl=30 # Leader锁30秒过期
|
110
|
-
)
|
111
|
-
|
112
|
-
await self.scheduler.connect()
|
113
|
-
logger.info("Scheduler setup completed")
|
114
|
-
|
115
|
-
async def run(self):
|
116
|
-
"""运行调度器"""
|
117
|
-
self.running = True
|
118
|
-
logger.info("Starting scheduler...")
|
119
|
-
|
120
|
-
try:
|
121
|
-
# 运行调度器
|
122
|
-
await self.scheduler.run()
|
123
|
-
except Exception as e:
|
124
|
-
logger.error(f"Scheduler error: {e}", exc_info=True)
|
125
|
-
finally:
|
126
|
-
self.running = False
|
127
|
-
logger.info("Scheduler stopped")
|
128
|
-
|
129
|
-
async def shutdown(self):
|
130
|
-
"""关闭调度器"""
|
131
|
-
logger.info("Shutting down scheduler...")
|
132
|
-
|
133
|
-
if self.scheduler:
|
134
|
-
self.scheduler.stop()
|
135
|
-
await self.scheduler.disconnect()
|
136
|
-
|
137
|
-
if self.app:
|
138
|
-
# 关闭应用连接
|
139
|
-
pass
|
140
|
-
|
141
|
-
logger.info("Scheduler shutdown completed")
|
142
|
-
|
143
|
-
def handle_signal(self, signum, frame):
|
144
|
-
"""处理终止信号"""
|
145
|
-
logger.info(f"Received signal {signum}, initiating shutdown...")
|
146
|
-
self.running = False
|
147
|
-
|
148
|
-
# 创建关闭任务
|
149
|
-
if self.scheduler:
|
150
|
-
self.scheduler.stop()
|
151
|
-
|
152
|
-
|
153
|
-
async def main():
|
154
|
-
"""主函数"""
|
155
|
-
runner = SchedulerRunner()
|
156
|
-
|
157
|
-
# 设置信号处理
|
158
|
-
signal.signal(signal.SIGINT, runner.handle_signal)
|
159
|
-
signal.signal(signal.SIGTERM, runner.handle_signal)
|
160
|
-
|
161
|
-
try:
|
162
|
-
# 初始化
|
163
|
-
await runner.setup()
|
164
|
-
|
165
|
-
# 运行调度器
|
166
|
-
await runner.run()
|
167
|
-
|
168
|
-
except KeyboardInterrupt:
|
169
|
-
logger.info("Received keyboard interrupt")
|
170
|
-
except Exception as e:
|
171
|
-
logger.error(f"Unexpected error: {e}", exc_info=True)
|
172
|
-
finally:
|
173
|
-
# 清理
|
174
|
-
await runner.shutdown()
|
175
|
-
|
176
|
-
|
177
|
-
if __name__ == '__main__':
|
178
|
-
# 设置日志级别
|
179
|
-
import logging
|
180
|
-
logging.basicConfig(
|
181
|
-
level=logging.INFO,
|
182
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
183
|
-
)
|
184
|
-
|
185
|
-
# 运行主函数
|
186
|
-
asyncio.run(main())
|
jettask/scheduler/schema.sql
DELETED
@@ -1,84 +0,0 @@
|
|
1
|
-
-- 定时任务表
|
2
|
-
CREATE TABLE IF NOT EXISTS scheduled_tasks (
|
3
|
-
id BIGSERIAL PRIMARY KEY, -- 自增主键(任务唯一标识)
|
4
|
-
scheduler_id VARCHAR(255) NOT NULL UNIQUE, -- 任务的唯一标识符(必填,用于去重)
|
5
|
-
task_name VARCHAR(255) NOT NULL, -- 要执行的函数名(对应@app.task注册的任务名)
|
6
|
-
task_type VARCHAR(50) NOT NULL, -- 任务类型: cron, interval, once
|
7
|
-
|
8
|
-
-- 任务执行相关
|
9
|
-
queue_name VARCHAR(100) NOT NULL, -- 目标队列名
|
10
|
-
task_args JSONB DEFAULT '[]', -- 任务参数
|
11
|
-
task_kwargs JSONB DEFAULT '{}', -- 任务关键字参数
|
12
|
-
|
13
|
-
-- 调度相关
|
14
|
-
cron_expression VARCHAR(100), -- cron表达式 (task_type=cron时使用)
|
15
|
-
interval_seconds NUMERIC(10,2), -- 间隔秒数 (task_type=interval时使用,支持小数)
|
16
|
-
next_run_time TIMESTAMP WITH TIME ZONE, -- 下次执行时间
|
17
|
-
last_run_time TIMESTAMP WITH TIME ZONE, -- 上次执行时间
|
18
|
-
|
19
|
-
-- 状态和控制
|
20
|
-
enabled BOOLEAN DEFAULT true, -- 是否启用
|
21
|
-
max_retries INTEGER DEFAULT 3, -- 最大重试次数
|
22
|
-
retry_delay INTEGER DEFAULT 60, -- 重试延迟(秒)
|
23
|
-
timeout INTEGER DEFAULT 300, -- 任务超时时间(秒)
|
24
|
-
priority INTEGER DEFAULT NULL, -- 任务优先级 (1=最高, 数字越大优先级越低,NULL=默认最低)
|
25
|
-
|
26
|
-
-- 元数据
|
27
|
-
description TEXT, -- 任务描述
|
28
|
-
tags JSONB DEFAULT '[]', -- 标签
|
29
|
-
metadata JSONB DEFAULT '{}', -- 额外元数据
|
30
|
-
|
31
|
-
-- 时间戳
|
32
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
33
|
-
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
34
|
-
);
|
35
|
-
|
36
|
-
-- 索引
|
37
|
-
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_next_run ON scheduled_tasks(next_run_time) WHERE enabled = true;
|
38
|
-
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_task_type ON scheduled_tasks(task_type);
|
39
|
-
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_queue ON scheduled_tasks(queue_name);
|
40
|
-
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_enabled ON scheduled_tasks(enabled);
|
41
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_scheduled_tasks_scheduler_id ON scheduled_tasks(scheduler_id);
|
42
|
-
|
43
|
-
-- 任务执行历史表
|
44
|
-
CREATE TABLE IF NOT EXISTS task_execution_history (
|
45
|
-
id BIGSERIAL PRIMARY KEY,
|
46
|
-
task_id BIGINT NOT NULL, -- 关联的任务ID(外键到 scheduled_tasks.id)
|
47
|
-
event_id VARCHAR(255) NOT NULL, -- 执行事件ID
|
48
|
-
|
49
|
-
-- 执行信息
|
50
|
-
scheduled_time TIMESTAMP WITH TIME ZONE NOT NULL, -- 计划执行时间
|
51
|
-
started_at TIMESTAMP WITH TIME ZONE, -- 实际开始时间
|
52
|
-
finished_at TIMESTAMP WITH TIME ZONE, -- 完成时间
|
53
|
-
|
54
|
-
-- 执行结果
|
55
|
-
status VARCHAR(50) NOT NULL, -- pending, running, success, failed, timeout
|
56
|
-
result JSONB, -- 执行结果
|
57
|
-
error_message TEXT, -- 错误信息
|
58
|
-
retry_count INTEGER DEFAULT 0, -- 重试次数
|
59
|
-
|
60
|
-
-- 性能指标
|
61
|
-
duration_ms INTEGER, -- 执行耗时(毫秒)
|
62
|
-
worker_id VARCHAR(100), -- 执行的worker ID
|
63
|
-
|
64
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
65
|
-
);
|
66
|
-
|
67
|
-
-- 索引
|
68
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_task_id ON task_execution_history(task_id);
|
69
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_event_id ON task_execution_history(event_id);
|
70
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_status ON task_execution_history(status);
|
71
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_scheduled ON task_execution_history(scheduled_time);
|
72
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_created ON task_execution_history(created_at);
|
73
|
-
|
74
|
-
-- 更新时间触发器
|
75
|
-
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
76
|
-
RETURNS TRIGGER AS $$
|
77
|
-
BEGIN
|
78
|
-
NEW.updated_at = CURRENT_TIMESTAMP;
|
79
|
-
RETURN NEW;
|
80
|
-
END;
|
81
|
-
$$ language 'plpgsql';
|
82
|
-
|
83
|
-
CREATE TRIGGER update_scheduled_tasks_updated_at BEFORE UPDATE
|
84
|
-
ON scheduled_tasks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
jettask/task/task_executor.py
DELETED
@@ -1,318 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
任务执行器
|
3
|
-
|
4
|
-
负责任务的执行、超时控制、错误处理
|
5
|
-
"""
|
6
|
-
|
7
|
-
from typing import List, Dict, Any, Optional
|
8
|
-
import asyncio
|
9
|
-
import logging
|
10
|
-
import traceback
|
11
|
-
from datetime import datetime
|
12
|
-
|
13
|
-
from .task_registry import TaskRegistry, TaskDefinition
|
14
|
-
|
15
|
-
logger = logging.getLogger('app')
|
16
|
-
|
17
|
-
|
18
|
-
class TaskExecutor:
|
19
|
-
"""
|
20
|
-
任务执行器
|
21
|
-
|
22
|
-
职责:
|
23
|
-
1. 任务执行
|
24
|
-
2. 超时控制
|
25
|
-
3. 错误处理
|
26
|
-
4. 结果存储
|
27
|
-
|
28
|
-
从ExecutorCore提取的纯任务执行逻辑
|
29
|
-
"""
|
30
|
-
|
31
|
-
def __init__(self,
|
32
|
-
task_registry: TaskRegistry,
|
33
|
-
data_access=None,
|
34
|
-
retry_manager=None):
|
35
|
-
"""
|
36
|
-
初始化任务执行器
|
37
|
-
|
38
|
-
Args:
|
39
|
-
task_registry: 任务注册器
|
40
|
-
data_access: 数据访问层(可选)
|
41
|
-
retry_manager: 重试管理器(可选)
|
42
|
-
"""
|
43
|
-
self.registry = task_registry
|
44
|
-
self.data_access = data_access
|
45
|
-
self.retry_manager = retry_manager
|
46
|
-
|
47
|
-
logger.debug("TaskExecutor initialized")
|
48
|
-
|
49
|
-
async def execute(self, message: dict) -> Any:
|
50
|
-
"""
|
51
|
-
执行任务
|
52
|
-
|
53
|
-
Args:
|
54
|
-
message: 任务消息
|
55
|
-
- task_name: 任务名称
|
56
|
-
- task_id: 任务ID
|
57
|
-
- args: 位置参数
|
58
|
-
- kwargs: 关键字参数
|
59
|
-
|
60
|
-
Returns:
|
61
|
-
Any: 任务执行结果
|
62
|
-
|
63
|
-
Raises:
|
64
|
-
ValueError: 任务不存在
|
65
|
-
asyncio.TimeoutError: 任务超时
|
66
|
-
Exception: 任务执行错误
|
67
|
-
"""
|
68
|
-
task_name = message.get('task_name')
|
69
|
-
task_id = message.get('task_id')
|
70
|
-
args = message.get('args', [])
|
71
|
-
kwargs = message.get('kwargs', {})
|
72
|
-
|
73
|
-
logger.debug(f"Executing task: {task_name} (id: {task_id})")
|
74
|
-
|
75
|
-
# 获取任务定义
|
76
|
-
task_def = self.registry.get(task_name)
|
77
|
-
if not task_def:
|
78
|
-
error_msg = f"Task {task_name} not found in registry"
|
79
|
-
logger.error(error_msg)
|
80
|
-
raise ValueError(error_msg)
|
81
|
-
|
82
|
-
# 执行任务(带超时)
|
83
|
-
try:
|
84
|
-
# 记录开始时间
|
85
|
-
start_time = datetime.now()
|
86
|
-
|
87
|
-
# 执行任务
|
88
|
-
if asyncio.iscoroutinefunction(task_def.func):
|
89
|
-
# 异步任务
|
90
|
-
result = await asyncio.wait_for(
|
91
|
-
task_def.func(*args, **kwargs),
|
92
|
-
timeout=task_def.timeout
|
93
|
-
)
|
94
|
-
else:
|
95
|
-
# 同步任务,在线程池中执行
|
96
|
-
loop = asyncio.get_event_loop()
|
97
|
-
result = await asyncio.wait_for(
|
98
|
-
loop.run_in_executor(None, task_def.func, *args, **kwargs),
|
99
|
-
timeout=task_def.timeout
|
100
|
-
)
|
101
|
-
|
102
|
-
# 计算执行时间
|
103
|
-
execution_time = (datetime.now() - start_time).total_seconds()
|
104
|
-
|
105
|
-
logger.info(
|
106
|
-
f"Task {task_name} (id: {task_id}) completed "
|
107
|
-
f"in {execution_time:.2f}s"
|
108
|
-
)
|
109
|
-
|
110
|
-
# 保存成功结果
|
111
|
-
if self.data_access:
|
112
|
-
await self.data_access.save_task_result(
|
113
|
-
task_id=task_id,
|
114
|
-
status='completed',
|
115
|
-
result=result,
|
116
|
-
execution_time=execution_time
|
117
|
-
)
|
118
|
-
|
119
|
-
return result
|
120
|
-
|
121
|
-
except asyncio.TimeoutError:
|
122
|
-
# 超时处理
|
123
|
-
logger.error(
|
124
|
-
f"Task {task_name} (id: {task_id}) timeout "
|
125
|
-
f"after {task_def.timeout}s"
|
126
|
-
)
|
127
|
-
await self._handle_timeout(task_id, task_def, message)
|
128
|
-
raise
|
129
|
-
|
130
|
-
except Exception as e:
|
131
|
-
# 错误处理
|
132
|
-
logger.error(
|
133
|
-
f"Task {task_name} (id: {task_id}) failed: {e}\n"
|
134
|
-
f"{traceback.format_exc()}"
|
135
|
-
)
|
136
|
-
await self._handle_error(task_id, task_def, message, e)
|
137
|
-
raise
|
138
|
-
|
139
|
-
async def _handle_timeout(self,
|
140
|
-
task_id: str,
|
141
|
-
task_def: TaskDefinition,
|
142
|
-
message: dict):
|
143
|
-
"""
|
144
|
-
处理超时
|
145
|
-
|
146
|
-
Args:
|
147
|
-
task_id: 任务ID
|
148
|
-
task_def: 任务定义
|
149
|
-
message: 任务消息
|
150
|
-
"""
|
151
|
-
# 保存超时状态
|
152
|
-
if self.data_access:
|
153
|
-
await self.data_access.save_task_result(
|
154
|
-
task_id=task_id,
|
155
|
-
status='timeout',
|
156
|
-
result=None,
|
157
|
-
error=f"Task timeout after {task_def.timeout}s"
|
158
|
-
)
|
159
|
-
|
160
|
-
# 重试逻辑
|
161
|
-
if task_def.max_retries > 0 and self.retry_manager:
|
162
|
-
current_retry = message.get('retry_count', 0)
|
163
|
-
if current_retry < task_def.max_retries:
|
164
|
-
logger.info(
|
165
|
-
f"Scheduling retry for task {task_def.name} "
|
166
|
-
f"(attempt {current_retry + 1}/{task_def.max_retries})"
|
167
|
-
)
|
168
|
-
await self.retry_manager.schedule_retry(
|
169
|
-
task_id=task_id,
|
170
|
-
task_def=task_def,
|
171
|
-
message=message,
|
172
|
-
retry_count=current_retry + 1,
|
173
|
-
delay=task_def.retry_delay
|
174
|
-
)
|
175
|
-
|
176
|
-
async def _handle_error(self,
|
177
|
-
task_id: str,
|
178
|
-
task_def: TaskDefinition,
|
179
|
-
message: dict,
|
180
|
-
error: Exception):
|
181
|
-
"""
|
182
|
-
处理错误
|
183
|
-
|
184
|
-
Args:
|
185
|
-
task_id: 任务ID
|
186
|
-
task_def: 任务定义
|
187
|
-
message: 任务消息
|
188
|
-
error: 错误对象
|
189
|
-
"""
|
190
|
-
# 保存失败状态
|
191
|
-
if self.data_access:
|
192
|
-
await self.data_access.save_task_result(
|
193
|
-
task_id=task_id,
|
194
|
-
status='failed',
|
195
|
-
result=None,
|
196
|
-
error=str(error),
|
197
|
-
traceback=traceback.format_exc()
|
198
|
-
)
|
199
|
-
|
200
|
-
# 重试逻辑
|
201
|
-
if task_def.max_retries > 0 and self.retry_manager:
|
202
|
-
current_retry = message.get('retry_count', 0)
|
203
|
-
if current_retry < task_def.max_retries:
|
204
|
-
logger.info(
|
205
|
-
f"Scheduling retry for task {task_def.name} "
|
206
|
-
f"(attempt {current_retry + 1}/{task_def.max_retries})"
|
207
|
-
)
|
208
|
-
await self.retry_manager.schedule_retry(
|
209
|
-
task_id=task_id,
|
210
|
-
task_def=task_def,
|
211
|
-
message=message,
|
212
|
-
retry_count=current_retry + 1,
|
213
|
-
delay=task_def.retry_delay
|
214
|
-
)
|
215
|
-
else:
|
216
|
-
logger.error(
|
217
|
-
f"Task {task_def.name} failed after {task_def.max_retries} retries"
|
218
|
-
)
|
219
|
-
|
220
|
-
async def batch_update_status(self, status_updates: List[dict]):
|
221
|
-
"""
|
222
|
-
批量更新任务状态
|
223
|
-
|
224
|
-
Args:
|
225
|
-
status_updates: 状态更新列表
|
226
|
-
- task_id: 任务ID
|
227
|
-
- status: 状态
|
228
|
-
- result: 结果(可选)
|
229
|
-
- error: 错误信息(可选)
|
230
|
-
"""
|
231
|
-
if not self.data_access:
|
232
|
-
logger.warning("No data_access configured, skipping batch status update")
|
233
|
-
return
|
234
|
-
|
235
|
-
for update in status_updates:
|
236
|
-
try:
|
237
|
-
await self.data_access.update_task_status(
|
238
|
-
task_id=update['task_id'],
|
239
|
-
status=update['status'],
|
240
|
-
result=update.get('result'),
|
241
|
-
error=update.get('error')
|
242
|
-
)
|
243
|
-
except Exception as e:
|
244
|
-
logger.error(f"Error updating task status: {e}")
|
245
|
-
|
246
|
-
async def execute_batch(self, messages: List[dict]) -> List[Dict[str, Any]]:
|
247
|
-
"""
|
248
|
-
批量执行任务
|
249
|
-
|
250
|
-
Args:
|
251
|
-
messages: 任务消息列表
|
252
|
-
|
253
|
-
Returns:
|
254
|
-
List[Dict]: 执行结果列表
|
255
|
-
- task_id: 任务ID
|
256
|
-
- status: 状态(completed/failed/timeout)
|
257
|
-
- result: 结果(可选)
|
258
|
-
- error: 错误信息(可选)
|
259
|
-
"""
|
260
|
-
results = []
|
261
|
-
|
262
|
-
# 并发执行所有任务
|
263
|
-
tasks = []
|
264
|
-
for message in messages:
|
265
|
-
task = asyncio.create_task(self._execute_with_result(message))
|
266
|
-
tasks.append(task)
|
267
|
-
|
268
|
-
# 等待所有任务完成
|
269
|
-
task_results = await asyncio.gather(*tasks, return_exceptions=True)
|
270
|
-
|
271
|
-
# 整理结果
|
272
|
-
for i, task_result in enumerate(task_results):
|
273
|
-
message = messages[i]
|
274
|
-
task_id = message.get('task_id')
|
275
|
-
|
276
|
-
if isinstance(task_result, Exception):
|
277
|
-
# 任务失败
|
278
|
-
if isinstance(task_result, asyncio.TimeoutError):
|
279
|
-
status = 'timeout'
|
280
|
-
error = 'Task timeout'
|
281
|
-
else:
|
282
|
-
status = 'failed'
|
283
|
-
error = str(task_result)
|
284
|
-
|
285
|
-
results.append({
|
286
|
-
'task_id': task_id,
|
287
|
-
'status': status,
|
288
|
-
'result': None,
|
289
|
-
'error': error
|
290
|
-
})
|
291
|
-
else:
|
292
|
-
# 任务成功
|
293
|
-
results.append({
|
294
|
-
'task_id': task_id,
|
295
|
-
'status': 'completed',
|
296
|
-
'result': task_result,
|
297
|
-
'error': None
|
298
|
-
})
|
299
|
-
|
300
|
-
return results
|
301
|
-
|
302
|
-
async def _execute_with_result(self, message: dict) -> Any:
|
303
|
-
"""
|
304
|
-
执行任务并返回结果(用于批量执行)
|
305
|
-
|
306
|
-
Args:
|
307
|
-
message: 任务消息
|
308
|
-
|
309
|
-
Returns:
|
310
|
-
Any: 任务执行结果
|
311
|
-
|
312
|
-
Raises:
|
313
|
-
Exception: 任务执行错误
|
314
|
-
"""
|
315
|
-
try:
|
316
|
-
return await self.execute(message)
|
317
|
-
except Exception:
|
318
|
-
raise # 让gather捕获异常
|