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.
- jettask/constants.py +213 -0
- jettask/core/app.py +525 -205
- jettask/core/cli.py +193 -185
- jettask/core/consumer_manager.py +126 -34
- jettask/core/context.py +3 -0
- jettask/core/enums.py +137 -0
- jettask/core/event_pool.py +501 -168
- jettask/core/message.py +147 -0
- jettask/core/offline_worker_recovery.py +181 -114
- jettask/core/task.py +10 -174
- jettask/core/task_batch.py +153 -0
- jettask/core/unified_manager_base.py +243 -0
- jettask/core/worker_scanner.py +54 -54
- jettask/executors/asyncio.py +184 -64
- jettask/webui/backend/config.py +51 -0
- jettask/webui/backend/data_access.py +2083 -92
- jettask/webui/backend/data_api.py +3294 -0
- jettask/webui/backend/dependencies.py +261 -0
- jettask/webui/backend/init_meta_db.py +158 -0
- jettask/webui/backend/main.py +1358 -69
- jettask/webui/backend/main_unified.py +78 -0
- jettask/webui/backend/main_v2.py +394 -0
- jettask/webui/backend/namespace_api.py +295 -0
- jettask/webui/backend/namespace_api_old.py +294 -0
- jettask/webui/backend/namespace_data_access.py +611 -0
- jettask/webui/backend/queue_backlog_api.py +727 -0
- jettask/webui/backend/queue_stats_v2.py +521 -0
- jettask/webui/backend/redis_monitor_api.py +476 -0
- jettask/webui/backend/unified_api_router.py +1601 -0
- jettask/webui/db_init.py +204 -32
- jettask/webui/frontend/package-lock.json +492 -1
- jettask/webui/frontend/package.json +4 -1
- jettask/webui/frontend/src/App.css +105 -7
- jettask/webui/frontend/src/App.jsx +49 -20
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
- jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
- jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
- jettask/webui/frontend/src/components/layout/Header.css +34 -10
- jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
- jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
- jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
- jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
- jettask/webui/frontend/src/main.jsx +1 -0
- jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
- jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
- jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
- jettask/webui/frontend/src/pages/Queues.jsx +5 -1
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
- jettask/webui/frontend/src/pages/Settings.jsx +800 -0
- jettask/webui/frontend/src/services/api.js +7 -5
- jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
- jettask/webui/frontend/src/utils/userPreferences.js +154 -0
- jettask/webui/multi_namespace_consumer.py +543 -0
- jettask/webui/pg_consumer.py +983 -246
- jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
- jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
- jettask/webui/static/dist/index.html +2 -2
- jettask/webui/task_center.py +216 -0
- jettask/webui/task_center_client.py +150 -0
- jettask/webui/unified_consumer_manager.py +193 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
- jettask-0.2.4.dist-info/RECORD +134 -0
- jettask/webui/pg_consumer_slow.py +0 -1099
- jettask/webui/pg_consumer_test.py +0 -678
- jettask/webui/static/dist/assets/index-823408e8.css +0 -1
- jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
- jettask/webui/test_pg_consumer_recovery.py +0 -547
- jettask/webui/test_recovery_simple.py +0 -492
- jettask/webui/test_self_recovery.py +0 -467
- jettask-0.2.1.dist-info/RECORD +0 -91
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/top_level.txt +0 -0
jettask/webui/db_init.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""数据库初始化工具"""
|
1
|
+
"""数据库初始化工具 - 支持分区表和优化索引"""
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import logging
|
@@ -6,7 +6,6 @@ import sys
|
|
6
6
|
from pathlib import Path
|
7
7
|
|
8
8
|
import asyncpg
|
9
|
-
from asyncpg.exceptions import PostgresError
|
10
9
|
|
11
10
|
from jettask.webui.config import PostgreSQLConfig
|
12
11
|
|
@@ -14,17 +13,16 @@ logger = logging.getLogger(__name__)
|
|
14
13
|
|
15
14
|
|
16
15
|
class DatabaseInitializer:
|
17
|
-
"""数据库初始化器"""
|
16
|
+
"""数据库初始化器 - 支持分区表"""
|
18
17
|
|
19
18
|
def __init__(self, pg_config: PostgreSQLConfig):
|
20
19
|
self.pg_config = pg_config
|
21
|
-
self.schema_path = Path(__file__).parent / "
|
20
|
+
self.schema_path = Path(__file__).parent / "sql" / "init_database.sql"
|
22
21
|
|
23
22
|
async def test_connection(self) -> bool:
|
24
23
|
"""测试数据库连接"""
|
25
24
|
try:
|
26
25
|
logger.info(f"正在测试数据库连接: {self.pg_config.host}:{self.pg_config.port}/{self.pg_config.database}")
|
27
|
-
|
28
26
|
conn = await asyncpg.connect(self.pg_config.dsn)
|
29
27
|
await conn.close()
|
30
28
|
|
@@ -64,7 +62,7 @@ class DatabaseInitializer:
|
|
64
62
|
return False
|
65
63
|
|
66
64
|
async def init_schema(self) -> bool:
|
67
|
-
"""
|
65
|
+
"""初始化数据库架构(支持分区表)"""
|
68
66
|
try:
|
69
67
|
if not self.schema_path.exists():
|
70
68
|
logger.error(f"✗ Schema文件不存在: {self.schema_path}")
|
@@ -76,24 +74,132 @@ class DatabaseInitializer:
|
|
76
74
|
logger.info("正在初始化数据库表结构...")
|
77
75
|
conn = await asyncpg.connect(self.pg_config.dsn)
|
78
76
|
|
79
|
-
#
|
80
|
-
|
77
|
+
# 智能分割SQL语句,处理函数定义中的$$符号
|
78
|
+
def split_sql_statements(sql_text):
|
79
|
+
"""智能分割SQL语句,正确处理$$定界符"""
|
80
|
+
statements = []
|
81
|
+
current_statement = []
|
82
|
+
in_dollar_quote = False
|
83
|
+
|
84
|
+
lines = sql_text.split('\n')
|
85
|
+
for line in lines:
|
86
|
+
# 跳过纯注释行
|
87
|
+
if line.strip().startswith('--') and not in_dollar_quote:
|
88
|
+
continue
|
89
|
+
|
90
|
+
current_statement.append(line)
|
91
|
+
|
92
|
+
# 检查$$定界符
|
93
|
+
if '$$' in line:
|
94
|
+
# 计算该行中$$的数量
|
95
|
+
dollar_count = line.count('$$')
|
96
|
+
if dollar_count % 2 == 1: # 奇数个$$,切换状态
|
97
|
+
in_dollar_quote = not in_dollar_quote
|
98
|
+
|
99
|
+
# 如果不在$$定界符内,且行以分号结尾,则语句结束
|
100
|
+
if not in_dollar_quote and line.rstrip().endswith(';'):
|
101
|
+
statement = '\n'.join(current_statement).strip()
|
102
|
+
if statement and not statement.startswith('--'):
|
103
|
+
statements.append(statement)
|
104
|
+
current_statement = []
|
105
|
+
|
106
|
+
# 处理最后一个语句(如果有)
|
107
|
+
if current_statement:
|
108
|
+
statement = '\n'.join(current_statement).strip()
|
109
|
+
if statement and not statement.startswith('--'):
|
110
|
+
statements.append(statement)
|
111
|
+
|
112
|
+
return statements
|
113
|
+
|
114
|
+
# 使用智能分割函数
|
115
|
+
statements = split_sql_statements(schema_sql)
|
116
|
+
|
117
|
+
for i, statement in enumerate(statements, 1):
|
118
|
+
if not statement:
|
119
|
+
continue
|
120
|
+
|
121
|
+
try:
|
122
|
+
# 处理 RAISE NOTICE(这些通常在函数内部,现在不会被错误分割)
|
123
|
+
if 'RAISE NOTICE' in statement and not 'CREATE' in statement:
|
124
|
+
logger.info("跳过独立的 RAISE NOTICE 语句")
|
125
|
+
continue
|
126
|
+
|
127
|
+
await conn.execute(statement)
|
128
|
+
|
129
|
+
# 记录重要操作
|
130
|
+
if 'CREATE TABLE' in statement:
|
131
|
+
if 'PARTITION BY' in statement:
|
132
|
+
table_name = statement.split('CREATE TABLE')[1].split('(')[0].strip().split(' ')[0]
|
133
|
+
logger.info(f" ✓ 创建分区表: {table_name}")
|
134
|
+
else:
|
135
|
+
table_name = statement.split('CREATE TABLE')[1].split('(')[0].strip().split(' ')[0]
|
136
|
+
logger.info(f" ✓ 创建表: {table_name}")
|
137
|
+
elif 'CREATE INDEX' in statement:
|
138
|
+
index_parts = statement.split('CREATE INDEX')[1].split('ON')[0].strip().split(' ')
|
139
|
+
index_name = index_parts[-1] if index_parts else 'unknown'
|
140
|
+
logger.info(f" ✓ 创建索引: {index_name}")
|
141
|
+
elif 'CREATE' in statement and 'FUNCTION' in statement:
|
142
|
+
func_parts = statement.split('FUNCTION')[1].split('(')[0].strip().split(' ')
|
143
|
+
func_name = func_parts[-1] if func_parts else 'unknown'
|
144
|
+
logger.info(f" ✓ 创建函数: {func_name}")
|
145
|
+
elif 'SELECT' in statement and 'partition' in statement.lower():
|
146
|
+
logger.info(f" ✓ 执行分区创建")
|
147
|
+
|
148
|
+
except Exception as e:
|
149
|
+
# 只在调试时显示部分语句内容
|
150
|
+
stmt_preview = statement[:200] if len(statement) > 200 else statement
|
151
|
+
logger.warning(f" 语句 {i} 执行警告: {str(e)[:100]}")
|
152
|
+
# 继续执行,因为可能是表已存在等非致命错误
|
81
153
|
|
82
|
-
#
|
154
|
+
# 验证核心表是否创建成功
|
83
155
|
tables = await conn.fetch("""
|
84
|
-
SELECT tablename
|
156
|
+
SELECT tablename,
|
157
|
+
CASE
|
158
|
+
WHEN c.relkind = 'p' THEN 'partitioned'
|
159
|
+
WHEN p.inhrelid IS NOT NULL THEN 'partition'
|
160
|
+
ELSE 'regular'
|
161
|
+
END as table_type
|
162
|
+
FROM pg_tables t
|
163
|
+
LEFT JOIN pg_class c ON c.relname = t.tablename AND c.relnamespace = 'public'::regnamespace
|
164
|
+
LEFT JOIN pg_inherits p ON p.inhrelid = c.oid
|
85
165
|
WHERE schemaname = 'public'
|
86
|
-
AND tablename IN ('tasks', '
|
166
|
+
AND (tablename IN ('tasks', 'task_runs', 'scheduled_tasks', 'namespaces',
|
167
|
+
'stream_backlog_monitor', 'alert_rules', 'alert_history')
|
168
|
+
OR tablename LIKE 'tasks_%'
|
169
|
+
OR tablename LIKE 'task_runs_%'
|
170
|
+
OR tablename LIKE 'stream_backlog_monitor_%')
|
87
171
|
ORDER BY tablename
|
88
172
|
""")
|
89
173
|
|
90
174
|
created_tables = [row['tablename'] for row in tables]
|
91
|
-
logger.info(f"✓ 成功创建表: {', '.join(created_tables)}")
|
92
175
|
|
93
|
-
|
176
|
+
logger.info("")
|
177
|
+
logger.info("=" * 50)
|
178
|
+
logger.info("✓ 成功创建的表:")
|
179
|
+
|
180
|
+
# 分类显示表
|
181
|
+
main_tables = []
|
182
|
+
partition_tables = []
|
183
|
+
|
94
184
|
for table in created_tables:
|
95
|
-
|
96
|
-
|
185
|
+
if '_2025_' in table or '_2024_' in table or '_2026_' in table:
|
186
|
+
partition_tables.append(table)
|
187
|
+
else:
|
188
|
+
main_tables.append(table)
|
189
|
+
|
190
|
+
logger.info(f" 主表: {', '.join(main_tables)}")
|
191
|
+
if partition_tables:
|
192
|
+
logger.info(f" 分区: {', '.join(partition_tables)}")
|
193
|
+
|
194
|
+
# 显示表记录数
|
195
|
+
logger.info("")
|
196
|
+
logger.info("表数据统计:")
|
197
|
+
for table in main_tables:
|
198
|
+
try:
|
199
|
+
count = await conn.fetchval(f"SELECT COUNT(*) FROM {table}")
|
200
|
+
logger.info(f" - {table}: {count} 条记录")
|
201
|
+
except:
|
202
|
+
pass # 分区主表可能不能直接查询
|
97
203
|
|
98
204
|
await conn.close()
|
99
205
|
return True
|
@@ -107,17 +213,20 @@ class DatabaseInitializer:
|
|
107
213
|
try:
|
108
214
|
conn = await asyncpg.connect(self.pg_config.dsn)
|
109
215
|
|
216
|
+
# 检查一个实际存在的表
|
217
|
+
test_table = 'scheduled_tasks' # 这个表一定存在
|
218
|
+
|
110
219
|
# 检查基本权限
|
111
220
|
permissions = await conn.fetch("""
|
112
|
-
SELECT has_table_privilege($1,
|
113
|
-
has_table_privilege($1,
|
114
|
-
has_table_privilege($1,
|
115
|
-
has_table_privilege($1,
|
116
|
-
""", self.pg_config.user)
|
221
|
+
SELECT has_table_privilege($1, $2, 'SELECT') as can_select,
|
222
|
+
has_table_privilege($1, $2, 'INSERT') as can_insert,
|
223
|
+
has_table_privilege($1, $2, 'UPDATE') as can_update,
|
224
|
+
has_table_privilege($1, $2, 'DELETE') as can_delete
|
225
|
+
""", self.pg_config.user, test_table)
|
117
226
|
|
118
227
|
if permissions:
|
119
228
|
perm = permissions[0]
|
120
|
-
logger.info(f"✓
|
229
|
+
logger.info(f"✓ 用户权限检查 (表: {test_table}):")
|
121
230
|
logger.info(f" - SELECT: {'✓' if perm['can_select'] else '✗'}")
|
122
231
|
logger.info(f" - INSERT: {'✓' if perm['can_insert'] else '✗'}")
|
123
232
|
logger.info(f" - UPDATE: {'✓' if perm['can_update'] else '✗'}")
|
@@ -130,11 +239,57 @@ class DatabaseInitializer:
|
|
130
239
|
logger.warning(f"权限检查失败: {e}")
|
131
240
|
return True # 不阻止继续
|
132
241
|
|
242
|
+
async def create_partitions(self) -> bool:
|
243
|
+
"""创建初始分区"""
|
244
|
+
try:
|
245
|
+
logger.info("")
|
246
|
+
logger.info("正在创建初始分区...")
|
247
|
+
conn = await asyncpg.connect(self.pg_config.dsn)
|
248
|
+
|
249
|
+
# 创建分区
|
250
|
+
partition_functions = [
|
251
|
+
'create_tasks_partition',
|
252
|
+
'create_task_runs_partition',
|
253
|
+
'create_stream_backlog_partition'
|
254
|
+
]
|
255
|
+
|
256
|
+
for func in partition_functions:
|
257
|
+
try:
|
258
|
+
await conn.execute(f"SELECT {func}()")
|
259
|
+
logger.info(f" ✓ 执行分区函数: {func}")
|
260
|
+
except Exception as e:
|
261
|
+
logger.warning(f" 分区函数 {func} 执行警告: {str(e)[:50]}")
|
262
|
+
|
263
|
+
# 查看创建的分区
|
264
|
+
partitions = await conn.fetch("""
|
265
|
+
SELECT
|
266
|
+
parent.relname as parent_table,
|
267
|
+
COUNT(*) as partition_count
|
268
|
+
FROM pg_inherits
|
269
|
+
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
|
270
|
+
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
|
271
|
+
WHERE parent.relname IN ('tasks', 'task_runs', 'stream_backlog_monitor')
|
272
|
+
GROUP BY parent.relname
|
273
|
+
""")
|
274
|
+
|
275
|
+
if partitions:
|
276
|
+
logger.info("")
|
277
|
+
logger.info("分区创建结果:")
|
278
|
+
for row in partitions:
|
279
|
+
logger.info(f" - {row['parent_table']}: {row['partition_count']} 个分区")
|
280
|
+
|
281
|
+
await conn.close()
|
282
|
+
return True
|
283
|
+
|
284
|
+
except Exception as e:
|
285
|
+
logger.error(f"创建分区失败: {e}")
|
286
|
+
return False
|
287
|
+
|
133
288
|
async def run(self) -> bool:
|
134
289
|
"""运行完整的初始化流程"""
|
135
|
-
logger.info("=" *
|
136
|
-
logger.info("
|
137
|
-
logger.info("=" *
|
290
|
+
logger.info("=" * 60)
|
291
|
+
logger.info("JetTask 数据库初始化 (支持分区表和优化索引)")
|
292
|
+
logger.info("=" * 60)
|
138
293
|
|
139
294
|
# 1. 创建数据库(如果需要)
|
140
295
|
if not await self.create_database():
|
@@ -144,19 +299,33 @@ class DatabaseInitializer:
|
|
144
299
|
if not await self.test_connection():
|
145
300
|
return False
|
146
301
|
|
147
|
-
# 3. 初始化schema
|
302
|
+
# 3. 初始化schema(包含分区表)
|
148
303
|
if not await self.init_schema():
|
149
304
|
return False
|
150
305
|
|
151
|
-
# 4.
|
306
|
+
# 4. 创建初始分区
|
307
|
+
await self.create_partitions()
|
308
|
+
|
309
|
+
# 5. 检查权限
|
152
310
|
await self.check_permissions()
|
153
311
|
|
154
|
-
logger.info("
|
312
|
+
logger.info("")
|
313
|
+
logger.info("=" * 60)
|
155
314
|
logger.info("✓ 数据库初始化完成!")
|
156
|
-
logger.info("=" *
|
315
|
+
logger.info("=" * 60)
|
157
316
|
logger.info("")
|
158
|
-
logger.info("
|
159
|
-
logger.info(
|
317
|
+
logger.info("特性说明:")
|
318
|
+
logger.info(" • tasks 和 task_runs 表已配置为按月分区")
|
319
|
+
logger.info(" • 索引已优化,删除冗余索引")
|
320
|
+
logger.info(" • 自动分区管理函数已创建")
|
321
|
+
logger.info("")
|
322
|
+
logger.info("维护建议:")
|
323
|
+
logger.info(" • 定期执行: SELECT maintain_tasks_partitions()")
|
324
|
+
logger.info(" • 定期执行: SELECT maintain_task_runs_partitions()")
|
325
|
+
logger.info(" • 建议配置 cron 任务自动维护分区")
|
326
|
+
logger.info("")
|
327
|
+
logger.info("您现在可以启动服务:")
|
328
|
+
logger.info(f" python -m jettask.webui.backend.main")
|
160
329
|
logger.info("")
|
161
330
|
|
162
331
|
return True
|
@@ -188,9 +357,12 @@ def init_database():
|
|
188
357
|
pg_config = PostgreSQLConfig(
|
189
358
|
host=os.getenv('JETTASK_PG_HOST', 'localhost'),
|
190
359
|
port=int(os.getenv('JETTASK_PG_PORT', '5432')),
|
191
|
-
database=os.getenv('
|
360
|
+
database=os.getenv('JETTASK_PG_DATABASE', 'jettask'),
|
192
361
|
user=os.getenv('JETTASK_PG_USER', 'jettask'),
|
193
362
|
password=os.getenv('JETTASK_PG_PASSWORD', '123456'),
|
194
363
|
)
|
195
364
|
|
196
|
-
asyncio.run(init_database_async(pg_config))
|
365
|
+
asyncio.run(init_database_async(pg_config))
|
366
|
+
|
367
|
+
if __name__ == "__main__":
|
368
|
+
init_database()
|