jettask 0.2.1__py3-none-any.whl → 0.2.4__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.
Files changed (89) hide show
  1. jettask/constants.py +213 -0
  2. jettask/core/app.py +525 -205
  3. jettask/core/cli.py +193 -185
  4. jettask/core/consumer_manager.py +126 -34
  5. jettask/core/context.py +3 -0
  6. jettask/core/enums.py +137 -0
  7. jettask/core/event_pool.py +501 -168
  8. jettask/core/message.py +147 -0
  9. jettask/core/offline_worker_recovery.py +181 -114
  10. jettask/core/task.py +10 -174
  11. jettask/core/task_batch.py +153 -0
  12. jettask/core/unified_manager_base.py +243 -0
  13. jettask/core/worker_scanner.py +54 -54
  14. jettask/executors/asyncio.py +184 -64
  15. jettask/webui/backend/config.py +51 -0
  16. jettask/webui/backend/data_access.py +2083 -92
  17. jettask/webui/backend/data_api.py +3294 -0
  18. jettask/webui/backend/dependencies.py +261 -0
  19. jettask/webui/backend/init_meta_db.py +158 -0
  20. jettask/webui/backend/main.py +1358 -69
  21. jettask/webui/backend/main_unified.py +78 -0
  22. jettask/webui/backend/main_v2.py +394 -0
  23. jettask/webui/backend/namespace_api.py +295 -0
  24. jettask/webui/backend/namespace_api_old.py +294 -0
  25. jettask/webui/backend/namespace_data_access.py +611 -0
  26. jettask/webui/backend/queue_backlog_api.py +727 -0
  27. jettask/webui/backend/queue_stats_v2.py +521 -0
  28. jettask/webui/backend/redis_monitor_api.py +476 -0
  29. jettask/webui/backend/unified_api_router.py +1601 -0
  30. jettask/webui/db_init.py +204 -32
  31. jettask/webui/frontend/package-lock.json +492 -1
  32. jettask/webui/frontend/package.json +4 -1
  33. jettask/webui/frontend/src/App.css +105 -7
  34. jettask/webui/frontend/src/App.jsx +49 -20
  35. jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
  36. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
  37. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
  38. jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
  39. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
  40. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
  41. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
  42. jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
  43. jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
  44. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
  45. jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
  46. jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
  47. jettask/webui/frontend/src/components/layout/Header.css +34 -10
  48. jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
  49. jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
  50. jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
  51. jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
  52. jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
  53. jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
  54. jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
  55. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
  56. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
  57. jettask/webui/frontend/src/main.jsx +1 -0
  58. jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
  59. jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
  60. jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
  61. jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
  62. jettask/webui/frontend/src/pages/Queues.jsx +5 -1
  63. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
  64. jettask/webui/frontend/src/pages/Settings.jsx +800 -0
  65. jettask/webui/frontend/src/services/api.js +7 -5
  66. jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
  67. jettask/webui/frontend/src/utils/userPreferences.js +154 -0
  68. jettask/webui/multi_namespace_consumer.py +543 -0
  69. jettask/webui/pg_consumer.py +983 -246
  70. jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
  71. jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
  72. jettask/webui/static/dist/index.html +2 -2
  73. jettask/webui/task_center.py +216 -0
  74. jettask/webui/task_center_client.py +150 -0
  75. jettask/webui/unified_consumer_manager.py +193 -0
  76. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
  77. jettask-0.2.4.dist-info/RECORD +134 -0
  78. jettask/webui/pg_consumer_slow.py +0 -1099
  79. jettask/webui/pg_consumer_test.py +0 -678
  80. jettask/webui/static/dist/assets/index-823408e8.css +0 -1
  81. jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
  82. jettask/webui/test_pg_consumer_recovery.py +0 -547
  83. jettask/webui/test_recovery_simple.py +0 -492
  84. jettask/webui/test_self_recovery.py +0 -467
  85. jettask-0.2.1.dist-info/RECORD +0 -91
  86. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
  87. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
  88. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
  89. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,261 @@
1
+ """
2
+ Dependency injection for JetTask WebUI Backend
3
+ """
4
+ from typing import Optional, AsyncGenerator
5
+ from fastapi import Depends, HTTPException, Header
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+ import redis.asyncio as redis
8
+
9
+ from core.database import db_manager, DatabaseManager
10
+ from core.cache import cache_manager, CacheManager
11
+ from core.exceptions import NamespaceNotFoundError, DatabaseConnectionError
12
+ from namespace_data_access import get_namespace_data_access
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ # 依赖注入函数
19
+
20
+ async def get_database_manager() -> DatabaseManager:
21
+ """获取数据库管理器"""
22
+ return db_manager
23
+
24
+
25
+ async def get_cache_manager() -> CacheManager:
26
+ """获取缓存管理器"""
27
+ return cache_manager
28
+
29
+
30
+ async def get_namespace_from_path(namespace: str = "default") -> str:
31
+ """从路径参数获取命名空间"""
32
+ if not namespace:
33
+ raise HTTPException(status_code=400, detail="Namespace is required")
34
+ return namespace
35
+
36
+
37
+ async def get_namespace_from_header(
38
+ x_namespace: Optional[str] = Header(None, alias="X-Namespace")
39
+ ) -> str:
40
+ """从请求头获取命名空间"""
41
+ return x_namespace or "default"
42
+
43
+
44
+ async def get_current_namespace(
45
+ path_namespace: str = Depends(get_namespace_from_path),
46
+ header_namespace: str = Depends(get_namespace_from_header)
47
+ ) -> str:
48
+ """获取当前命名空间(优先使用路径参数)"""
49
+ return path_namespace if path_namespace != "default" else header_namespace
50
+
51
+
52
+ async def validate_namespace(namespace: str) -> str:
53
+ """验证命名空间是否存在"""
54
+ try:
55
+ namespace_data_access = get_namespace_data_access()
56
+ # 尝试获取命名空间连接来验证其存在
57
+ conn = await namespace_data_access.manager.get_connection(namespace)
58
+ if not conn:
59
+ raise NamespaceNotFoundError(namespace)
60
+ return namespace
61
+ except Exception as e:
62
+ logger.error(f"命名空间验证失败 {namespace}: {e}")
63
+ raise NamespaceNotFoundError(namespace)
64
+
65
+
66
+ async def get_validated_namespace(
67
+ namespace: str = Depends(get_current_namespace)
68
+ ) -> str:
69
+ """获取并验证命名空间"""
70
+ return await validate_namespace(namespace)
71
+
72
+
73
+ async def get_pg_connection(
74
+ namespace: str = Depends(get_validated_namespace)
75
+ ) -> AsyncGenerator[object, None]:
76
+ """获取PostgreSQL连接"""
77
+ try:
78
+ namespace_data_access = get_namespace_data_access()
79
+ conn = await namespace_data_access.manager.get_connection(namespace)
80
+
81
+ if not conn.AsyncSessionLocal:
82
+ raise DatabaseConnectionError("PostgreSQL not configured for this namespace")
83
+
84
+ async with conn.AsyncSessionLocal() as session:
85
+ yield session
86
+
87
+ except DatabaseConnectionError:
88
+ raise
89
+ except Exception as e:
90
+ logger.error(f"获取PostgreSQL连接失败: {e}")
91
+ raise DatabaseConnectionError(f"PostgreSQL connection failed: {e}")
92
+
93
+
94
+ async def get_redis_client(
95
+ namespace: str = Depends(get_validated_namespace)
96
+ ) -> AsyncGenerator[redis.Redis, None]:
97
+ """获取Redis客户端"""
98
+ try:
99
+ namespace_data_access = get_namespace_data_access()
100
+ conn = await namespace_data_access.manager.get_connection(namespace)
101
+
102
+ redis_client = await conn.get_redis_client(decode=False)
103
+
104
+ try:
105
+ yield redis_client
106
+ finally:
107
+ await redis_client.aclose()
108
+
109
+ except Exception as e:
110
+ logger.error(f"获取Redis客户端失败: {e}")
111
+ raise DatabaseConnectionError(f"Redis connection failed: {e}")
112
+
113
+
114
+ async def get_namespace_connection(
115
+ namespace: str = Depends(get_validated_namespace)
116
+ ):
117
+ """获取命名空间连接对象"""
118
+ try:
119
+ namespace_data_access = get_namespace_data_access()
120
+ return await namespace_data_access.manager.get_connection(namespace)
121
+ except Exception as e:
122
+ logger.error(f"获取命名空间连接失败: {e}")
123
+ raise DatabaseConnectionError(f"Namespace connection failed: {e}")
124
+
125
+
126
+ # 权限和认证相关依赖(预留)
127
+
128
+ async def get_current_user(
129
+ authorization: Optional[str] = Header(None)
130
+ ) -> Optional[dict]:
131
+ """获取当前用户(预留接口)"""
132
+ # 这里可以实现JWT令牌解析、用户认证等逻辑
133
+ # 目前返回None表示匿名用户
134
+ return None
135
+
136
+
137
+ async def require_auth(
138
+ current_user: Optional[dict] = Depends(get_current_user)
139
+ ) -> dict:
140
+ """要求用户认证(预留接口)"""
141
+ # 这里可以实现认证检查逻辑
142
+ # 目前直接返回匿名用户
143
+ return current_user or {"id": "anonymous", "role": "user"}
144
+
145
+
146
+ async def require_admin(
147
+ current_user: dict = Depends(require_auth)
148
+ ) -> dict:
149
+ """要求管理员权限(预留接口)"""
150
+ # 这里可以实现权限检查逻辑
151
+ # 目前直接返回用户
152
+ return current_user
153
+
154
+
155
+ # 请求验证相关依赖
156
+
157
+ def validate_page_params(page: int = 1, page_size: int = 20) -> tuple[int, int]:
158
+ """验证分页参数"""
159
+ if page < 1:
160
+ raise HTTPException(status_code=400, detail="Page must be >= 1")
161
+ if page_size < 1 or page_size > 100:
162
+ raise HTTPException(status_code=400, detail="Page size must be between 1 and 100")
163
+ return page, page_size
164
+
165
+
166
+ def validate_time_range(
167
+ start_time: Optional[str] = None,
168
+ end_time: Optional[str] = None,
169
+ time_range: Optional[str] = None
170
+ ) -> dict:
171
+ """验证时间范围参数"""
172
+ from datetime import datetime, timedelta
173
+
174
+ result = {}
175
+
176
+ if time_range:
177
+ # 预设时间范围
178
+ time_map = {
179
+ '15m': timedelta(minutes=15),
180
+ '30m': timedelta(minutes=30),
181
+ '1h': timedelta(hours=1),
182
+ '3h': timedelta(hours=3),
183
+ '6h': timedelta(hours=6),
184
+ '12h': timedelta(hours=12),
185
+ '24h': timedelta(hours=24),
186
+ '3d': timedelta(days=3),
187
+ '7d': timedelta(days=7),
188
+ '30d': timedelta(days=30)
189
+ }
190
+
191
+ if time_range not in time_map:
192
+ raise HTTPException(
193
+ status_code=400,
194
+ detail=f"Invalid time_range: {time_range}"
195
+ )
196
+
197
+ now = datetime.now()
198
+ result['end_time'] = now
199
+ result['start_time'] = now - time_map[time_range]
200
+ result['time_range'] = time_range
201
+
202
+ elif start_time or end_time:
203
+ # 自定义时间范围
204
+ try:
205
+ if start_time:
206
+ result['start_time'] = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
207
+ if end_time:
208
+ result['end_time'] = datetime.fromisoformat(end_time.replace('Z', '+00:00'))
209
+
210
+ if result.get('start_time') and result.get('end_time'):
211
+ if result['start_time'] >= result['end_time']:
212
+ raise HTTPException(
213
+ status_code=400,
214
+ detail="start_time must be before end_time"
215
+ )
216
+ except ValueError as e:
217
+ raise HTTPException(
218
+ status_code=400,
219
+ detail=f"Invalid datetime format: {e}"
220
+ )
221
+
222
+ else:
223
+ # 默认最近1小时
224
+ now = datetime.now()
225
+ result['end_time'] = now
226
+ result['start_time'] = now - timedelta(hours=1)
227
+ result['time_range'] = '1h'
228
+
229
+ return result
230
+
231
+
232
+ # 性能监控相关依赖
233
+
234
+ class RequestMetrics:
235
+ """请求指标收集器"""
236
+
237
+ def __init__(self):
238
+ self.start_time = None
239
+ self.namespace = None
240
+ self.endpoint = None
241
+
242
+ def start(self, namespace: str, endpoint: str):
243
+ import time
244
+ self.start_time = time.time()
245
+ self.namespace = namespace
246
+ self.endpoint = endpoint
247
+
248
+ def finish(self):
249
+ if self.start_time:
250
+ import time
251
+ duration = time.time() - self.start_time
252
+ logger.info(
253
+ f"API请求完成: {self.endpoint} "
254
+ f"namespace={self.namespace} "
255
+ f"duration={duration:.3f}s"
256
+ )
257
+
258
+
259
+ async def get_request_metrics() -> RequestMetrics:
260
+ """获取请求指标收集器"""
261
+ return RequestMetrics()
@@ -0,0 +1,158 @@
1
+ """
2
+ 初始化任务中心元数据库
3
+ 这个脚本用于创建任务中心自己的数据库和表结构
4
+ """
5
+ import psycopg2
6
+ import sys
7
+ from config import task_center_config
8
+ import logging
9
+
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def create_meta_database():
15
+ """创建任务中心元数据库"""
16
+ # 连接到postgres数据库来创建新数据库
17
+ conn = psycopg2.connect(
18
+ host=task_center_config.meta_db_host,
19
+ port=task_center_config.meta_db_port,
20
+ user=task_center_config.meta_db_user,
21
+ password=task_center_config.meta_db_password,
22
+ database='postgres'
23
+ )
24
+ conn.autocommit = True
25
+ cur = conn.cursor()
26
+
27
+ try:
28
+ # 检查数据库是否存在
29
+ cur.execute(
30
+ "SELECT 1 FROM pg_database WHERE datname = %s",
31
+ (task_center_config.meta_db_name,)
32
+ )
33
+
34
+ if not cur.fetchone():
35
+ # 创建数据库
36
+ cur.execute(f"CREATE DATABASE {task_center_config.meta_db_name}")
37
+ logger.info(f"Created database: {task_center_config.meta_db_name}")
38
+ else:
39
+ logger.info(f"Database already exists: {task_center_config.meta_db_name}")
40
+
41
+ finally:
42
+ cur.close()
43
+ conn.close()
44
+
45
+
46
+ def init_meta_tables():
47
+ """初始化元数据表"""
48
+ conn = psycopg2.connect(task_center_config.sync_meta_database_url)
49
+ cur = conn.cursor()
50
+
51
+ try:
52
+ # 创建命名空间表
53
+ cur.execute("""
54
+ CREATE TABLE IF NOT EXISTS namespaces (
55
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
56
+ name VARCHAR(255) NOT NULL UNIQUE,
57
+ description TEXT,
58
+ -- JetTask应用使用的Redis配置
59
+ redis_config JSONB NOT NULL COMMENT 'JetTask应用的Redis配置',
60
+ -- JetTask应用使用的PostgreSQL配置
61
+ pg_config JSONB NOT NULL COMMENT 'JetTask应用的PostgreSQL配置',
62
+ is_active BOOLEAN DEFAULT true,
63
+ version INTEGER DEFAULT 1,
64
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
65
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
66
+ );
67
+ """)
68
+
69
+ # 创建索引
70
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_namespaces_name ON namespaces(name);")
71
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_namespaces_is_active ON namespaces(is_active);")
72
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_namespaces_version ON namespaces(version);")
73
+
74
+ # 创建更新时间触发器
75
+ cur.execute("""
76
+ CREATE OR REPLACE FUNCTION update_namespaces_updated_at()
77
+ RETURNS TRIGGER AS $$
78
+ BEGIN
79
+ NEW.updated_at = CURRENT_TIMESTAMP;
80
+ RETURN NEW;
81
+ END;
82
+ $$ LANGUAGE plpgsql;
83
+ """)
84
+
85
+ cur.execute("""
86
+ DO $$
87
+ BEGIN
88
+ IF NOT EXISTS (
89
+ SELECT 1 FROM pg_trigger
90
+ WHERE tgname = 'update_namespaces_updated_at'
91
+ ) THEN
92
+ CREATE TRIGGER update_namespaces_updated_at
93
+ BEFORE UPDATE ON namespaces
94
+ FOR EACH ROW
95
+ EXECUTE FUNCTION update_namespaces_updated_at();
96
+ END IF;
97
+ END;
98
+ $$;
99
+ """)
100
+
101
+ # 插入默认命名空间
102
+ cur.execute("""
103
+ INSERT INTO namespaces (id, name, description, redis_config, pg_config)
104
+ VALUES (
105
+ 'a8f10720-068b-4264-bcbb-e00ceba370e9',
106
+ 'default',
107
+ '默认命名空间 - JetTask应用使用此配置',
108
+ '{"host": "localhost", "port": 6379, "password": null, "db": 0}',
109
+ '{"host": "localhost", "port": 5432, "user": "jettask", "password": "123456", "database": "jettask"}'
110
+ )
111
+ ON CONFLICT (name) DO NOTHING;
112
+ """)
113
+
114
+ conn.commit()
115
+ logger.info("Initialized meta tables successfully")
116
+
117
+ except Exception as e:
118
+ conn.rollback()
119
+ logger.error(f"Failed to initialize meta tables: {e}")
120
+ raise
121
+ finally:
122
+ cur.close()
123
+ conn.close()
124
+
125
+
126
+ def main():
127
+ """主函数"""
128
+ logger.info("=" * 60)
129
+ logger.info("任务中心元数据库初始化")
130
+ logger.info("=" * 60)
131
+ logger.info(f"元数据库地址: {task_center_config.meta_db_host}:{task_center_config.meta_db_port}")
132
+ logger.info(f"元数据库名称: {task_center_config.meta_db_name}")
133
+ logger.info(f"元数据库用户: {task_center_config.meta_db_user}")
134
+ logger.info("-" * 60)
135
+
136
+ try:
137
+ # 1. 创建数据库
138
+ create_meta_database()
139
+
140
+ # 2. 初始化表结构
141
+ init_meta_tables()
142
+
143
+ logger.info("-" * 60)
144
+ logger.info("✅ 元数据库初始化完成!")
145
+ logger.info("")
146
+ logger.info("说明:")
147
+ logger.info("1. 任务中心元数据库用于存储命名空间配置")
148
+ logger.info("2. 每个命名空间定义了JetTask应用使用的Redis和PostgreSQL配置")
149
+ logger.info("3. JetTask应用通过命名空间ID获取其专用的数据库配置")
150
+ logger.info("=" * 60)
151
+
152
+ except Exception as e:
153
+ logger.error(f"初始化失败: {e}")
154
+ sys.exit(1)
155
+
156
+
157
+ if __name__ == "__main__":
158
+ main()