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
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 / "schema.sql"
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
- # 执行schema
80
- await conn.execute(schema_sql)
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 FROM pg_tables
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', 'queue_stats', 'workers')
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
- count = await conn.fetchval(f"SELECT COUNT(*) FROM {table}")
96
- logger.info(f" - {table}: {count} 条记录")
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, 'tasks', 'SELECT') as can_select,
113
- has_table_privilege($1, 'tasks', 'INSERT') as can_insert,
114
- has_table_privilege($1, 'tasks', 'UPDATE') as can_update,
115
- has_table_privilege($1, 'tasks', 'DELETE') as can_delete
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("=" * 50)
136
- logger.info("开始初始化PostgreSQL数据库")
137
- logger.info("=" * 50)
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("=" * 50)
312
+ logger.info("")
313
+ logger.info("=" * 60)
155
314
  logger.info("✓ 数据库初始化完成!")
156
- logger.info("=" * 50)
315
+ logger.info("=" * 60)
157
316
  logger.info("")
158
- logger.info("您现在可以启动WebUI:")
159
- logger.info(f" python -m jettask.webui --pg-url {self.pg_config.dsn}")
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('JETTASK_PG_DB', 'jettask'),
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()