jettask 0.2.15__py3-none-any.whl → 0.2.17__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 +14 -35
- jettask/{webui/__main__.py → __main__.py} +4 -4
- jettask/api/__init__.py +103 -0
- jettask/api/v1/__init__.py +29 -0
- jettask/api/v1/alerts.py +226 -0
- jettask/api/v1/analytics.py +323 -0
- jettask/api/v1/namespaces.py +134 -0
- jettask/api/v1/overview.py +136 -0
- jettask/api/v1/queues.py +530 -0
- jettask/api/v1/scheduled.py +420 -0
- jettask/api/v1/settings.py +44 -0
- jettask/{webui/api.py → api.py} +4 -46
- jettask/{webui/backend → backend}/main.py +21 -109
- jettask/{webui/backend → backend}/main_unified.py +1 -1
- jettask/{webui/backend → backend}/namespace_api_old.py +3 -30
- jettask/{webui/backend → backend}/namespace_data_access.py +2 -1
- jettask/{webui/backend → backend}/unified_api_router.py +14 -74
- jettask/{core/cli.py → cli.py} +106 -26
- jettask/config/nacos_config.py +386 -0
- jettask/core/app.py +8 -100
- jettask/core/db_manager.py +515 -0
- jettask/core/event_pool.py +5 -2
- jettask/core/unified_manager_base.py +59 -14
- jettask/{webui/db_init.py → db_init.py} +1 -1
- jettask/executors/asyncio.py +2 -2
- jettask/{webui/integrated_gradio_app.py → integrated_gradio_app.py} +1 -1
- jettask/{webui/multi_namespace_consumer.py → multi_namespace_consumer.py} +5 -2
- jettask/{webui/pg_consumer.py → pg_consumer.py} +137 -69
- jettask/{webui/run.py → run.py} +1 -1
- jettask/{webui/run_webui.py → run_webui.py} +4 -4
- jettask/scheduler/manager.py +6 -0
- jettask/scheduler/multi_namespace_scheduler.py +2 -2
- jettask/scheduler/unified_manager.py +5 -5
- jettask/scheduler/unified_scheduler_manager.py +20 -12
- jettask/schemas/__init__.py +166 -0
- jettask/schemas/alert.py +99 -0
- jettask/schemas/backlog.py +122 -0
- jettask/schemas/common.py +139 -0
- jettask/schemas/monitoring.py +181 -0
- jettask/schemas/namespace.py +168 -0
- jettask/schemas/queue.py +83 -0
- jettask/schemas/scheduled_task.py +128 -0
- jettask/schemas/task.py +70 -0
- jettask/services/__init__.py +24 -0
- jettask/services/alert_service.py +454 -0
- jettask/services/analytics_service.py +46 -0
- jettask/services/overview_service.py +978 -0
- jettask/services/queue_service.py +711 -0
- jettask/services/redis_monitor_service.py +151 -0
- jettask/services/scheduled_task_service.py +207 -0
- jettask/services/settings_service.py +758 -0
- jettask/services/task_service.py +157 -0
- jettask/{webui/task_center.py → task_center.py} +30 -8
- jettask/{webui/task_center_client.py → task_center_client.py} +1 -1
- jettask/{webui/config.py → webui_config.py} +6 -1
- jettask/webui_exceptions.py +67 -0
- jettask/webui_sql/verify_database.sql +72 -0
- {jettask-0.2.15.dist-info → jettask-0.2.17.dist-info}/METADATA +2 -1
- jettask-0.2.17.dist-info/RECORD +150 -0
- {jettask-0.2.15.dist-info → jettask-0.2.17.dist-info}/entry_points.txt +1 -1
- jettask/webui/backend/data_api.py +0 -3294
- jettask/webui/backend/namespace_api.py +0 -295
- jettask/webui/backend/queue_backlog_api.py +0 -727
- jettask/webui/backend/redis_monitor_api.py +0 -476
- jettask/webui/frontend/index.html +0 -13
- jettask/webui/frontend/package.json +0 -30
- jettask/webui/frontend/src/App.css +0 -109
- jettask/webui/frontend/src/App.jsx +0 -66
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +0 -166
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +0 -298
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +0 -638
- jettask/webui/frontend/src/components/QueueDetailsTable.css +0 -65
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +0 -487
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +0 -465
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +0 -423
- jettask/webui/frontend/src/components/TaskFilter.jsx +0 -425
- jettask/webui/frontend/src/components/TimeRangeSelector.css +0 -21
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +0 -160
- jettask/webui/frontend/src/components/charts/QueueChart.jsx +0 -111
- jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +0 -115
- jettask/webui/frontend/src/components/charts/WorkerChart.jsx +0 -40
- jettask/webui/frontend/src/components/common/StatsCard.jsx +0 -18
- jettask/webui/frontend/src/components/layout/AppLayout.css +0 -95
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +0 -49
- jettask/webui/frontend/src/components/layout/Header.css +0 -106
- jettask/webui/frontend/src/components/layout/Header.jsx +0 -106
- jettask/webui/frontend/src/components/layout/SideMenu.css +0 -137
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +0 -209
- jettask/webui/frontend/src/components/layout/TabsNav.css +0 -244
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +0 -206
- jettask/webui/frontend/src/components/layout/UserInfo.css +0 -197
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +0 -197
- jettask/webui/frontend/src/contexts/LoadingContext.jsx +0 -27
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +0 -72
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +0 -245
- jettask/webui/frontend/src/index.css +0 -114
- jettask/webui/frontend/src/main.jsx +0 -22
- jettask/webui/frontend/src/pages/Alerts.jsx +0 -684
- jettask/webui/frontend/src/pages/Dashboard/index.css +0 -35
- jettask/webui/frontend/src/pages/Dashboard/index.jsx +0 -281
- jettask/webui/frontend/src/pages/Dashboard.jsx +0 -1330
- jettask/webui/frontend/src/pages/QueueDetail.jsx +0 -1117
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +0 -527
- jettask/webui/frontend/src/pages/Queues.jsx +0 -12
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +0 -810
- jettask/webui/frontend/src/pages/Settings.jsx +0 -801
- jettask/webui/frontend/src/pages/Workers.jsx +0 -12
- jettask/webui/frontend/src/services/api.js +0 -159
- jettask/webui/frontend/src/services/queueTrend.js +0 -166
- jettask/webui/frontend/src/utils/suppressWarnings.js +0 -22
- jettask/webui/frontend/src/utils/userPreferences.js +0 -154
- jettask/webui/frontend/vite.config.js +0 -26
- jettask/webui/sql/init_database.sql +0 -640
- jettask-0.2.15.dist-info/RECORD +0 -172
- /jettask/{webui/backend → backend}/__init__.py +0 -0
- /jettask/{webui/backend → backend}/api/__init__.py +0 -0
- /jettask/{webui/backend → backend}/api/v1/__init__.py +0 -0
- /jettask/{webui/backend → backend}/api/v1/monitoring.py +0 -0
- /jettask/{webui/backend → backend}/api/v1/namespaces.py +0 -0
- /jettask/{webui/backend → backend}/api/v1/queues.py +0 -0
- /jettask/{webui/backend → backend}/api/v1/tasks.py +0 -0
- /jettask/{webui/backend → backend}/config.py +0 -0
- /jettask/{webui/backend → backend}/core/__init__.py +0 -0
- /jettask/{webui/backend → backend}/core/cache.py +0 -0
- /jettask/{webui/backend → backend}/core/database.py +0 -0
- /jettask/{webui/backend → backend}/core/exceptions.py +0 -0
- /jettask/{webui/backend → backend}/data_access.py +0 -0
- /jettask/{webui/backend → backend}/dependencies.py +0 -0
- /jettask/{webui/backend → backend}/init_meta_db.py +0 -0
- /jettask/{webui/backend → backend}/main_v2.py +0 -0
- /jettask/{webui/backend → backend}/models/__init__.py +0 -0
- /jettask/{webui/backend → backend}/models/requests.py +0 -0
- /jettask/{webui/backend → backend}/models/responses.py +0 -0
- /jettask/{webui/backend → backend}/queue_stats_v2.py +0 -0
- /jettask/{webui/backend → backend}/services/__init__.py +0 -0
- /jettask/{webui/backend → backend}/start.py +0 -0
- /jettask/{webui/cleanup_deprecated_tables.sql → cleanup_deprecated_tables.sql} +0 -0
- /jettask/{webui/gradio_app.py → gradio_app.py} +0 -0
- /jettask/{webui/__init__.py → main.py} +0 -0
- /jettask/{webui/models.py → models.py} +0 -0
- /jettask/{webui/run_monitor.py → run_monitor.py} +0 -0
- /jettask/{webui/schema.sql → schema.sql} +0 -0
- /jettask/{webui/unified_consumer_manager.py → unified_consumer_manager.py} +0 -0
- /jettask/{webui/models → webui_models}/__init__.py +0 -0
- /jettask/{webui/models → webui_models}/namespace.py +0 -0
- /jettask/{webui/sql → webui_sql}/batch_upsert_functions.sql +0 -0
- {jettask-0.2.15.dist-info → jettask-0.2.17.dist-info}/WHEEL +0 -0
- {jettask-0.2.15.dist-info → jettask-0.2.17.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.15.dist-info → jettask-0.2.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""
|
|
2
|
+
定时任务模块 - 定时任务管理、执行和监控
|
|
3
|
+
提供轻量级的路由入口,业务逻辑在 ScheduledTaskService 中实现
|
|
4
|
+
"""
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Request, Query
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from jettask.schemas import ScheduledTaskRequest
|
|
10
|
+
from jettask.services.scheduled_task_service import ScheduledTaskService
|
|
11
|
+
|
|
12
|
+
router = APIRouter(prefix="/scheduled", tags=["scheduled"])
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ============ 定时任务管理 ============
|
|
17
|
+
|
|
18
|
+
@router.get("/")
|
|
19
|
+
async def get_scheduled_tasks(
|
|
20
|
+
request: Request,
|
|
21
|
+
page: int = Query(1, ge=1),
|
|
22
|
+
page_size: int = Query(20, ge=1, le=100),
|
|
23
|
+
search: Optional[str] = Query(None, description="搜索关键字"),
|
|
24
|
+
is_active: Optional[bool] = Query(None, description="是否激活")
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
获取定时任务列表
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
page: 页码(从1开始)
|
|
31
|
+
page_size: 每页数量
|
|
32
|
+
search: 搜索关键字
|
|
33
|
+
is_active: 是否激活
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
app = request.app
|
|
37
|
+
if not app or not hasattr(app.state, 'data_access'):
|
|
38
|
+
raise HTTPException(status_code=500, detail="Data access not initialized")
|
|
39
|
+
|
|
40
|
+
data_access = app.state.data_access
|
|
41
|
+
|
|
42
|
+
async with data_access.get_session() as session:
|
|
43
|
+
tasks, total = await data_access.fetch_scheduled_tasks(
|
|
44
|
+
session=session,
|
|
45
|
+
page=page,
|
|
46
|
+
page_size=page_size,
|
|
47
|
+
search=search,
|
|
48
|
+
is_active=is_active
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"success": True,
|
|
53
|
+
"data": tasks,
|
|
54
|
+
"total": total,
|
|
55
|
+
"page": page,
|
|
56
|
+
"page_size": page_size
|
|
57
|
+
}
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"获取定时任务列表失败: {e}")
|
|
60
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@router.post("/filter")
|
|
64
|
+
async def get_scheduled_tasks_with_filters(request: Request):
|
|
65
|
+
"""
|
|
66
|
+
获取定时任务列表(支持高级筛选)
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
app = request.app
|
|
70
|
+
if not app or not hasattr(app.state, 'data_access'):
|
|
71
|
+
raise HTTPException(status_code=500, detail="Data access not initialized")
|
|
72
|
+
|
|
73
|
+
data_access = app.state.data_access
|
|
74
|
+
|
|
75
|
+
# 解析请求体
|
|
76
|
+
body = await request.json()
|
|
77
|
+
|
|
78
|
+
page = body.get('page', 1)
|
|
79
|
+
page_size = body.get('page_size', 20)
|
|
80
|
+
search = body.get('search')
|
|
81
|
+
is_active = body.get('is_active')
|
|
82
|
+
filters = body.get('filters', [])
|
|
83
|
+
time_range = body.get('time_range')
|
|
84
|
+
start_time = body.get('start_time')
|
|
85
|
+
end_time = body.get('end_time')
|
|
86
|
+
|
|
87
|
+
async with data_access.get_session() as session:
|
|
88
|
+
tasks, total = await data_access.fetch_scheduled_tasks(
|
|
89
|
+
session=session,
|
|
90
|
+
page=page,
|
|
91
|
+
page_size=page_size,
|
|
92
|
+
search=search,
|
|
93
|
+
is_active=is_active,
|
|
94
|
+
filters=filters,
|
|
95
|
+
time_range=time_range,
|
|
96
|
+
start_time=start_time,
|
|
97
|
+
end_time=end_time
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"success": True,
|
|
102
|
+
"data": tasks,
|
|
103
|
+
"total": total,
|
|
104
|
+
"page": page,
|
|
105
|
+
"page_size": page_size
|
|
106
|
+
}
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"获取定时任务列表失败: {e}")
|
|
109
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.post("/", status_code=201)
|
|
113
|
+
async def create_scheduled_task(request: Request, task_request: ScheduledTaskRequest):
|
|
114
|
+
"""
|
|
115
|
+
创建定时任务
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
task_request: 定时任务创建请求
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
app = request.app
|
|
122
|
+
if not app or not hasattr(app.state, 'data_access'):
|
|
123
|
+
raise HTTPException(status_code=500, detail="Data access not initialized")
|
|
124
|
+
|
|
125
|
+
data_access = app.state.data_access
|
|
126
|
+
|
|
127
|
+
# 验证调度配置
|
|
128
|
+
ScheduledTaskService.validate_schedule_config(
|
|
129
|
+
task_request.schedule_type,
|
|
130
|
+
task_request.schedule_config
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
task_data = {
|
|
134
|
+
"namespace": task_request.namespace,
|
|
135
|
+
"name": task_request.name,
|
|
136
|
+
"queue_name": task_request.queue_name,
|
|
137
|
+
"task_data": task_request.task_data,
|
|
138
|
+
"schedule_type": task_request.schedule_type,
|
|
139
|
+
"schedule_config": task_request.schedule_config,
|
|
140
|
+
"is_active": task_request.is_active,
|
|
141
|
+
"description": task_request.description,
|
|
142
|
+
"max_retry": task_request.max_retry,
|
|
143
|
+
"timeout": task_request.timeout
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async with data_access.get_session() as session:
|
|
147
|
+
task = await data_access.create_scheduled_task(session, task_data)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"success": True,
|
|
151
|
+
"data": task,
|
|
152
|
+
"message": "定时任务创建成功"
|
|
153
|
+
}
|
|
154
|
+
except ValueError as e:
|
|
155
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.error(f"创建定时任务失败: {e}")
|
|
158
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@router.put("/{task_id}")
|
|
162
|
+
async def update_scheduled_task(request: Request, task_id: str, task_request: ScheduledTaskRequest):
|
|
163
|
+
"""
|
|
164
|
+
更新定时任务
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
task_id: 任务ID
|
|
168
|
+
task_request: 定时任务更新请求
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
app = request.app
|
|
172
|
+
if not app or not hasattr(app.state, 'data_access'):
|
|
173
|
+
raise HTTPException(status_code=500, detail="Data access not initialized")
|
|
174
|
+
|
|
175
|
+
data_access = app.state.data_access
|
|
176
|
+
|
|
177
|
+
# 验证调度配置
|
|
178
|
+
ScheduledTaskService.validate_schedule_config(
|
|
179
|
+
task_request.schedule_type,
|
|
180
|
+
task_request.schedule_config
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
task_data = {
|
|
184
|
+
"namespace": task_request.namespace,
|
|
185
|
+
"name": task_request.name,
|
|
186
|
+
"queue_name": task_request.queue_name,
|
|
187
|
+
"task_data": task_request.task_data,
|
|
188
|
+
"schedule_type": task_request.schedule_type,
|
|
189
|
+
"schedule_config": task_request.schedule_config,
|
|
190
|
+
"is_active": task_request.is_active,
|
|
191
|
+
"description": task_request.description,
|
|
192
|
+
"max_retry": task_request.max_retry,
|
|
193
|
+
"timeout": task_request.timeout
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async with data_access.get_session() as session:
|
|
197
|
+
task = await data_access.update_scheduled_task(session, task_id, task_data)
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
"success": True,
|
|
201
|
+
"data": task,
|
|
202
|
+
"message": "定时任务更新成功"
|
|
203
|
+
}
|
|
204
|
+
except ValueError as e:
|
|
205
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.error(f"更新定时任务失败: {e}")
|
|
208
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@router.delete("/{task_id}")
|
|
212
|
+
async def delete_scheduled_task(request: Request, task_id: str):
|
|
213
|
+
"""
|
|
214
|
+
删除定时任务
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
task_id: 任务ID
|
|
218
|
+
"""
|
|
219
|
+
try:
|
|
220
|
+
app = request.app
|
|
221
|
+
if not app or not hasattr(app.state, 'data_access'):
|
|
222
|
+
raise HTTPException(status_code=500, detail="Data access not initialized")
|
|
223
|
+
|
|
224
|
+
data_access = app.state.data_access
|
|
225
|
+
|
|
226
|
+
async with data_access.get_session() as session:
|
|
227
|
+
success = await data_access.delete_scheduled_task(session, task_id)
|
|
228
|
+
|
|
229
|
+
if success:
|
|
230
|
+
return {
|
|
231
|
+
"success": True,
|
|
232
|
+
"message": f"定时任务 {task_id} 已删除"
|
|
233
|
+
}
|
|
234
|
+
else:
|
|
235
|
+
raise HTTPException(status_code=404, detail="定时任务不存在")
|
|
236
|
+
except HTTPException:
|
|
237
|
+
raise
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.error(f"删除定时任务失败: {e}")
|
|
240
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@router.post("/{task_id}/toggle")
|
|
244
|
+
async def toggle_scheduled_task(request: Request, task_id: str):
|
|
245
|
+
"""
|
|
246
|
+
启用/禁用定时任务
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
task_id: 任务ID
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
app = request.app
|
|
253
|
+
if not app or not hasattr(app.state, 'data_access'):
|
|
254
|
+
raise HTTPException(status_code=500, detail="Data access not initialized")
|
|
255
|
+
|
|
256
|
+
data_access = app.state.data_access
|
|
257
|
+
|
|
258
|
+
async with data_access.get_session() as session:
|
|
259
|
+
task = await data_access.toggle_scheduled_task(session, task_id)
|
|
260
|
+
|
|
261
|
+
if task:
|
|
262
|
+
return {
|
|
263
|
+
"success": True,
|
|
264
|
+
"data": {
|
|
265
|
+
"id": task["id"],
|
|
266
|
+
"is_active": task["enabled"] # 映射 enabled 到 is_active
|
|
267
|
+
},
|
|
268
|
+
"message": "定时任务状态已更新"
|
|
269
|
+
}
|
|
270
|
+
else:
|
|
271
|
+
raise HTTPException(status_code=404, detail="定时任务不存在")
|
|
272
|
+
except HTTPException:
|
|
273
|
+
raise
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"切换定时任务状态失败: {e}")
|
|
276
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@router.post("/{task_id}/execute")
|
|
280
|
+
async def execute_scheduled_task_now(request: Request, task_id: str):
|
|
281
|
+
"""
|
|
282
|
+
立即执行定时任务
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
task_id: 任务ID
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
app = request.app
|
|
289
|
+
if not app or not hasattr(app.state, 'data_access'):
|
|
290
|
+
raise HTTPException(status_code=500, detail="Data access not initialized")
|
|
291
|
+
|
|
292
|
+
if not hasattr(app.state, 'namespace_data_access'):
|
|
293
|
+
raise HTTPException(status_code=500, detail="Namespace data access not initialized")
|
|
294
|
+
|
|
295
|
+
data_access = app.state.data_access
|
|
296
|
+
namespace_data_access = app.state.namespace_data_access
|
|
297
|
+
|
|
298
|
+
# 调用服务层执行任务
|
|
299
|
+
result = await ScheduledTaskService.execute_task_now(
|
|
300
|
+
data_access,
|
|
301
|
+
namespace_data_access,
|
|
302
|
+
task_id
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return result
|
|
306
|
+
except ValueError as e:
|
|
307
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
308
|
+
except RuntimeError as e:
|
|
309
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.error(f"执行定时任务失败: {e}")
|
|
312
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ============ 执行历史和统计 ============
|
|
316
|
+
|
|
317
|
+
@router.get("/{task_id}/history")
|
|
318
|
+
async def get_scheduled_task_history(
|
|
319
|
+
request: Request,
|
|
320
|
+
task_id: str,
|
|
321
|
+
page: int = Query(1, ge=1),
|
|
322
|
+
page_size: int = Query(20, ge=1, le=100)
|
|
323
|
+
):
|
|
324
|
+
"""
|
|
325
|
+
获取定时任务执行历史
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
task_id: 任务ID
|
|
329
|
+
page: 页码
|
|
330
|
+
page_size: 每页数量
|
|
331
|
+
"""
|
|
332
|
+
try:
|
|
333
|
+
app = request.app
|
|
334
|
+
if not app or not hasattr(app.state, 'data_access'):
|
|
335
|
+
raise HTTPException(status_code=500, detail="Data access not initialized")
|
|
336
|
+
|
|
337
|
+
data_access = app.state.data_access
|
|
338
|
+
|
|
339
|
+
async with data_access.get_session() as session:
|
|
340
|
+
history, total = await data_access.fetch_task_execution_history(
|
|
341
|
+
session=session,
|
|
342
|
+
task_id=task_id,
|
|
343
|
+
page=page,
|
|
344
|
+
page_size=page_size
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
"success": True,
|
|
349
|
+
"data": history,
|
|
350
|
+
"total": total,
|
|
351
|
+
"page": page,
|
|
352
|
+
"page_size": page_size
|
|
353
|
+
}
|
|
354
|
+
except Exception as e:
|
|
355
|
+
logger.error(f"获取定时任务执行历史失败: {e}")
|
|
356
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@router.get("/{task_id}/trend")
|
|
360
|
+
async def get_scheduled_task_execution_trend(
|
|
361
|
+
request: Request,
|
|
362
|
+
task_id: str,
|
|
363
|
+
time_range: str = Query("7d", description="时间范围")
|
|
364
|
+
):
|
|
365
|
+
"""
|
|
366
|
+
获取定时任务执行趋势
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
task_id: 任务ID
|
|
370
|
+
time_range: 时间范围
|
|
371
|
+
"""
|
|
372
|
+
try:
|
|
373
|
+
app = request.app
|
|
374
|
+
if not app or not hasattr(app.state, 'data_access'):
|
|
375
|
+
raise HTTPException(status_code=500, detail="Data access not initialized")
|
|
376
|
+
|
|
377
|
+
data_access = app.state.data_access
|
|
378
|
+
|
|
379
|
+
async with data_access.get_session() as session:
|
|
380
|
+
data = await data_access.fetch_task_execution_trend(
|
|
381
|
+
session=session,
|
|
382
|
+
task_id=task_id,
|
|
383
|
+
time_range=time_range
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
"success": True,
|
|
388
|
+
"data": data,
|
|
389
|
+
"time_range": time_range
|
|
390
|
+
}
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logger.error(f"获取定时任务执行趋势失败: {e}")
|
|
393
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@router.get("/statistics/{namespace}")
|
|
397
|
+
async def get_scheduled_tasks_statistics(request: Request, namespace: str):
|
|
398
|
+
"""
|
|
399
|
+
获取定时任务统计数据
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
namespace: 命名空间
|
|
403
|
+
"""
|
|
404
|
+
try:
|
|
405
|
+
app = request.app
|
|
406
|
+
if not app or not hasattr(app.state, 'data_access'):
|
|
407
|
+
raise HTTPException(status_code=500, detail="Data access not initialized")
|
|
408
|
+
|
|
409
|
+
data_access = app.state.data_access
|
|
410
|
+
|
|
411
|
+
async with data_access.AsyncSessionLocal() as session:
|
|
412
|
+
# 获取统计数据,传递命名空间参数
|
|
413
|
+
stats = await data_access.get_scheduled_tasks_statistics(session, namespace)
|
|
414
|
+
return stats
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.error(f"获取定时任务统计失败: {e}")
|
|
417
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
__all__ = ['router']
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
设置模块 - 系统配置
|
|
3
|
+
提供轻量级的路由入口,业务逻辑在 SettingsService 中实现
|
|
4
|
+
"""
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
import logging
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
from jettask.services.settings_service import SettingsService
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# 创建设置模块路由,添加 /settings 前缀
|
|
14
|
+
router = APIRouter(prefix="/settings", tags=["settings"])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ============ 系统配置接口 ============
|
|
18
|
+
|
|
19
|
+
@router.get("/system")
|
|
20
|
+
async def get_system_settings():
|
|
21
|
+
"""
|
|
22
|
+
获取系统配置信息
|
|
23
|
+
返回系统级别的配置,如数据库连接信息、API配置等
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
return SettingsService.get_system_settings()
|
|
27
|
+
except Exception as e:
|
|
28
|
+
logger.error(f"获取系统配置失败: {e}")
|
|
29
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get("/database-status")
|
|
33
|
+
async def check_database_status():
|
|
34
|
+
"""
|
|
35
|
+
检查数据库连接状态
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
return await SettingsService.check_database_status()
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.error(f"数据库状态检查失败: {e}")
|
|
41
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = ['router']
|
jettask/{webui/api.py → api.py}
RENAMED
|
@@ -17,9 +17,9 @@ from sqlalchemy.orm import sessionmaker
|
|
|
17
17
|
from sqlalchemy import select, func, and_, or_, text
|
|
18
18
|
from sqlalchemy.dialects import postgresql
|
|
19
19
|
|
|
20
|
-
from jettask.
|
|
21
|
-
from jettask.
|
|
22
|
-
from jettask.
|
|
20
|
+
from jettask.pg_consumer import PostgreSQLConsumer
|
|
21
|
+
from jettask.webui_config import PostgreSQLConfig, RedisConfig
|
|
22
|
+
from jettask.models import Base, Task, QueueStats, Worker
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
@@ -236,44 +236,7 @@ class RedisMonitor:
|
|
|
236
236
|
except Exception as e:
|
|
237
237
|
print(f"Error reading from stream {prefixed_queue_name}: {e}")
|
|
238
238
|
return None
|
|
239
|
-
|
|
240
|
-
async def get_all_tasks(self) -> List[Dict[str, Any]]:
|
|
241
|
-
"""获取所有任务状态"""
|
|
242
|
-
tasks = []
|
|
243
|
-
|
|
244
|
-
# 扫描所有状态键
|
|
245
|
-
cursor = 0
|
|
246
|
-
pattern = f"{self.redis_prefix}:STATUS:*"
|
|
247
|
-
|
|
248
|
-
while True:
|
|
249
|
-
cursor, keys = await self.redis.scan(
|
|
250
|
-
cursor, match=pattern, count=100
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
for status_key in keys:
|
|
254
|
-
event_id = status_key.split(":")[-1]
|
|
255
|
-
task_info = await self.get_task_info(event_id)
|
|
256
|
-
|
|
257
|
-
# 尝试获取Stream信息
|
|
258
|
-
# 这里需要知道任务所在的队列,可能需要从状态信息中解析
|
|
259
|
-
if task_info.get("status"):
|
|
260
|
-
try:
|
|
261
|
-
status_data = json.loads(task_info["status"])
|
|
262
|
-
queue_name = status_data.get("queue")
|
|
263
|
-
if queue_name:
|
|
264
|
-
stream_info = await self.get_stream_info(queue_name, event_id)
|
|
265
|
-
if stream_info:
|
|
266
|
-
task_info["stream_info"] = stream_info
|
|
267
|
-
except:
|
|
268
|
-
pass
|
|
269
|
-
|
|
270
|
-
tasks.append(task_info)
|
|
271
|
-
|
|
272
|
-
if cursor == 0:
|
|
273
|
-
break
|
|
274
|
-
|
|
275
|
-
return tasks
|
|
276
|
-
|
|
239
|
+
|
|
277
240
|
async def get_queue_tasks(self, queue_name: str, start_time: Optional[str] = None,
|
|
278
241
|
end_time: Optional[str] = None, limit: int = 100) -> Dict[str, Any]:
|
|
279
242
|
"""获取指定队列的任务(基于时间范围)
|
|
@@ -1318,11 +1281,6 @@ async def lifespan(app: FastAPI):
|
|
|
1318
1281
|
|
|
1319
1282
|
app = FastAPI(title="Jettask Monitor", lifespan=lifespan)
|
|
1320
1283
|
|
|
1321
|
-
@app.get("/api/tasks")
|
|
1322
|
-
async def get_tasks():
|
|
1323
|
-
"""获取所有任务"""
|
|
1324
|
-
tasks = await monitor.get_all_tasks()
|
|
1325
|
-
return {"tasks": tasks}
|
|
1326
1284
|
|
|
1327
1285
|
@app.get("/api/queue/{queue_name}/tasks")
|
|
1328
1286
|
async def get_queue_tasks(
|