jettask 0.2.23__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.
Files changed (110) hide show
  1. jettask/__init__.py +2 -0
  2. jettask/cli.py +12 -8
  3. jettask/config/lua_scripts.py +37 -0
  4. jettask/config/nacos_config.py +1 -1
  5. jettask/core/app.py +313 -340
  6. jettask/core/container.py +4 -4
  7. jettask/{persistence → core}/namespace.py +93 -27
  8. jettask/core/task.py +16 -9
  9. jettask/core/unified_manager_base.py +136 -26
  10. jettask/db/__init__.py +67 -0
  11. jettask/db/base.py +137 -0
  12. jettask/{utils/db_connector.py → db/connector.py} +130 -26
  13. jettask/db/models/__init__.py +16 -0
  14. jettask/db/models/scheduled_task.py +196 -0
  15. jettask/db/models/task.py +77 -0
  16. jettask/db/models/task_run.py +85 -0
  17. jettask/executor/__init__.py +0 -15
  18. jettask/executor/core.py +76 -31
  19. jettask/executor/process_entry.py +29 -114
  20. jettask/executor/task_executor.py +4 -0
  21. jettask/messaging/event_pool.py +928 -685
  22. jettask/messaging/scanner.py +30 -0
  23. jettask/persistence/__init__.py +28 -103
  24. jettask/persistence/buffer.py +170 -0
  25. jettask/persistence/consumer.py +330 -249
  26. jettask/persistence/manager.py +304 -0
  27. jettask/persistence/persistence.py +391 -0
  28. jettask/scheduler/__init__.py +15 -3
  29. jettask/scheduler/{task_crud.py → database.py} +61 -57
  30. jettask/scheduler/loader.py +2 -2
  31. jettask/scheduler/{scheduler_coordinator.py → manager.py} +23 -6
  32. jettask/scheduler/models.py +14 -10
  33. jettask/scheduler/schedule.py +166 -0
  34. jettask/scheduler/scheduler.py +12 -11
  35. jettask/schemas/__init__.py +50 -1
  36. jettask/schemas/backlog.py +43 -6
  37. jettask/schemas/namespace.py +70 -19
  38. jettask/schemas/queue.py +19 -3
  39. jettask/schemas/responses.py +493 -0
  40. jettask/task/__init__.py +0 -2
  41. jettask/task/router.py +3 -0
  42. jettask/test_connection_monitor.py +1 -1
  43. jettask/utils/__init__.py +7 -5
  44. jettask/utils/db_init.py +8 -4
  45. jettask/utils/namespace_dep.py +167 -0
  46. jettask/utils/queue_matcher.py +186 -0
  47. jettask/utils/rate_limit/concurrency_limiter.py +7 -1
  48. jettask/utils/stream_backlog.py +1 -1
  49. jettask/webui/__init__.py +0 -1
  50. jettask/webui/api/__init__.py +4 -4
  51. jettask/webui/api/alerts.py +806 -71
  52. jettask/webui/api/example_refactored.py +400 -0
  53. jettask/webui/api/namespaces.py +390 -45
  54. jettask/webui/api/overview.py +300 -54
  55. jettask/webui/api/queues.py +971 -267
  56. jettask/webui/api/scheduled.py +1249 -56
  57. jettask/webui/api/settings.py +129 -7
  58. jettask/webui/api/workers.py +442 -0
  59. jettask/webui/app.py +46 -2329
  60. jettask/webui/middleware/__init__.py +6 -0
  61. jettask/webui/middleware/namespace_middleware.py +135 -0
  62. jettask/webui/services/__init__.py +146 -0
  63. jettask/webui/services/heartbeat_service.py +251 -0
  64. jettask/webui/services/overview_service.py +60 -51
  65. jettask/webui/services/queue_monitor_service.py +426 -0
  66. jettask/webui/services/redis_monitor_service.py +87 -0
  67. jettask/webui/services/settings_service.py +174 -111
  68. jettask/webui/services/task_monitor_service.py +222 -0
  69. jettask/webui/services/timeline_pg_service.py +452 -0
  70. jettask/webui/services/timeline_service.py +189 -0
  71. jettask/webui/services/worker_monitor_service.py +467 -0
  72. jettask/webui/utils/__init__.py +11 -0
  73. jettask/webui/utils/time_utils.py +122 -0
  74. jettask/worker/lifecycle.py +8 -2
  75. {jettask-0.2.23.dist-info → jettask-0.2.24.dist-info}/METADATA +1 -1
  76. jettask-0.2.24.dist-info/RECORD +142 -0
  77. jettask/executor/executor.py +0 -338
  78. jettask/persistence/backlog_monitor.py +0 -567
  79. jettask/persistence/base.py +0 -2334
  80. jettask/persistence/db_manager.py +0 -516
  81. jettask/persistence/maintenance.py +0 -81
  82. jettask/persistence/message_consumer.py +0 -259
  83. jettask/persistence/models.py +0 -49
  84. jettask/persistence/offline_recovery.py +0 -196
  85. jettask/persistence/queue_discovery.py +0 -215
  86. jettask/persistence/task_persistence.py +0 -218
  87. jettask/persistence/task_updater.py +0 -583
  88. jettask/scheduler/add_execution_count.sql +0 -11
  89. jettask/scheduler/add_priority_field.sql +0 -26
  90. jettask/scheduler/add_scheduler_id.sql +0 -25
  91. jettask/scheduler/add_scheduler_id_index.sql +0 -10
  92. jettask/scheduler/make_scheduler_id_required.sql +0 -28
  93. jettask/scheduler/migrate_interval_seconds.sql +0 -9
  94. jettask/scheduler/performance_optimization.sql +0 -45
  95. jettask/scheduler/run_scheduler.py +0 -186
  96. jettask/scheduler/schema.sql +0 -84
  97. jettask/task/task_executor.py +0 -318
  98. jettask/webui/api/analytics.py +0 -323
  99. jettask/webui/config.py +0 -90
  100. jettask/webui/models/__init__.py +0 -3
  101. jettask/webui/models/namespace.py +0 -63
  102. jettask/webui/namespace_manager/__init__.py +0 -10
  103. jettask/webui/namespace_manager/multi.py +0 -593
  104. jettask/webui/namespace_manager/unified.py +0 -193
  105. jettask/webui/run.py +0 -46
  106. jettask-0.2.23.dist-info/RECORD +0 -145
  107. {jettask-0.2.23.dist-info → jettask-0.2.24.dist-info}/WHEEL +0 -0
  108. {jettask-0.2.23.dist-info → jettask-0.2.24.dist-info}/entry_points.txt +0 -0
  109. {jettask-0.2.23.dist-info → jettask-0.2.24.dist-info}/licenses/LICENSE +0 -0
  110. {jettask-0.2.23.dist-info → jettask-0.2.24.dist-info}/top_level.txt +0 -0
@@ -1,593 +0,0 @@
1
- #!/usr/bin/env python
2
- """多命名空间消费者管理器
3
-
4
- 为每个命名空间启动独立的pg_consumer进程
5
- 支持动态添加/移除命名空间
6
- """
7
-
8
- import asyncio
9
- import logging
10
- import multiprocessing as mp
11
- import signal
12
- import sys
13
- import os
14
- from typing import Dict, Optional, Set
15
- from datetime import datetime
16
-
17
- # 添加项目路径
18
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
19
-
20
- from jettask.persistence import PostgreSQLConsumer
21
- from jettask.webui.config import PostgreSQLConfig, RedisConfig
22
- # ConsumerStrategy 已移除,现在只使用 HEARTBEAT 策略
23
- from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
24
- from sqlalchemy.orm import sessionmaker
25
- from sqlalchemy import text
26
-
27
- logging.basicConfig(
28
- level=logging.INFO,
29
- format='%(asctime)s - %(process)d - %(name)s - %(levelname)s - %(message)s'
30
- )
31
- logger = logging.getLogger(__name__)
32
-
33
-
34
- class NamespaceConsumerProcess:
35
- """单个命名空间的消费进程"""
36
-
37
- def __init__(self, namespace_info: dict):
38
- """
39
- Args:
40
- namespace_info: 命名空间配置信息,包含:
41
- - id: 命名空间ID
42
- - name: 命名空间名称
43
- - redis_config: Redis配置
44
- - pg_config: PostgreSQL配置
45
- - redis_prefix: Redis键前缀
46
- """
47
- self.namespace_info = namespace_info
48
- self.process: Optional[mp.Process] = None
49
- self.shutdown_event = mp.Event() # 用于优雅退出的事件
50
-
51
- def start(self):
52
- """启动进程"""
53
- if self.process and self.process.is_alive():
54
- logger.warning(f"命名空间 {self.namespace_info['name']} 的消费进程已在运行")
55
- return
56
-
57
- self.process = mp.Process(
58
- target=self._run_consumer,
59
- name=f"consumer_{self.namespace_info['name']}"
60
- )
61
- self.process.daemon = False
62
- self.process.start()
63
- logger.info(f"启动命名空间 {self.namespace_info['name']} 的消费进程, PID: {self.process.pid}")
64
-
65
- def stop(self):
66
- """停止进程"""
67
- if self.process and self.process.is_alive():
68
- logger.info(f"停止命名空间 {self.namespace_info['name']} 的消费进程")
69
- # 先发送优雅退出信号
70
- self.shutdown_event.set()
71
- # 等待进程正常退出(7秒:5秒清理超时 + 2秒缓冲)
72
- self.process.join(timeout=7)
73
- if self.process.is_alive():
74
- logger.warning(f"等待优雅退出超时(7秒),发送 SIGTERM 信号")
75
- self.process.terminate()
76
- self.process.join(timeout=3)
77
- if self.process.is_alive():
78
- logger.warning(f"SIGTERM 超时(3秒),强制 SIGKILL")
79
- self.process.kill()
80
- self.process.join(timeout=2)
81
- if self.process.is_alive():
82
- logger.error(f"SIGKILL 后进程仍然存活,可能存在严重问题")
83
-
84
- def is_alive(self) -> bool:
85
- """检查进程是否存活"""
86
- return self.process and self.process.is_alive()
87
-
88
- def _run_consumer(self):
89
- """在子进程中运行消费者"""
90
- # 设置信号处理
91
- signal.signal(signal.SIGTERM, self._signal_handler)
92
- signal.signal(signal.SIGINT, self._signal_handler)
93
-
94
- # 创建新的事件循环
95
- loop = asyncio.new_event_loop()
96
- asyncio.set_event_loop(loop)
97
-
98
- consumer = None
99
- try:
100
- # 传递 consumer 引用以便在信号处理中使用
101
- consumer = loop.run_until_complete(self._async_run())
102
- except KeyboardInterrupt:
103
- logger.info(f"命名空间 {self.namespace_info['name']} 的消费进程收到中断信号")
104
- except Exception as e:
105
- logger.error(f"命名空间 {self.namespace_info['name']} 的消费进程异常退出: {e}", exc_info=True)
106
- finally:
107
- # 确保执行清理(带超时)
108
- if consumer:
109
- try:
110
- logger.info(f"执行命名空间 {self.namespace_info['name']} 的消费者清理")
111
- # 创建一个带超时的任务
112
- async def cleanup_with_timeout():
113
- try:
114
- await asyncio.wait_for(consumer.stop(), timeout=5.0)
115
- except asyncio.TimeoutError:
116
- logger.warning(f"外层清理超时(5秒),强制退出")
117
-
118
- loop.run_until_complete(cleanup_with_timeout())
119
- except Exception as e:
120
- logger.error(f"清理消费者时出错: {e}")
121
- loop.close()
122
-
123
- async def _async_run(self):
124
- """异步运行消费者"""
125
- namespace_name = self.namespace_info.get('name', 'unknown')
126
- logger.info(f"开始初始化命名空间 {namespace_name} 的消费者")
127
-
128
- consumer = None
129
- try:
130
- # 创建配置
131
- pg_config_data = self.namespace_info.get('pg_config', {})
132
-
133
- # 如果包含 url 字段,使用 from_url 方法
134
- if 'url' in pg_config_data:
135
- pg_config = PostgreSQLConfig.from_url(pg_config_data['url'])
136
- else:
137
- # 移除不支持的字段
138
- valid_fields = {'host', 'port', 'database', 'user', 'password'}
139
- filtered_config = {k: v for k, v in pg_config_data.items() if k in valid_fields}
140
- pg_config = PostgreSQLConfig(**filtered_config)
141
-
142
- redis_config_data = self.namespace_info.get('redis_config', {})
143
-
144
- # 如果包含 url 字段,解析它
145
- if 'url' in redis_config_data:
146
- from urllib.parse import urlparse
147
- parsed = urlparse(redis_config_data['url'])
148
- redis_config = RedisConfig(
149
- host=parsed.hostname or 'localhost',
150
- port=parsed.port or 6379,
151
- db=int(parsed.path.lstrip('/')) if parsed.path and parsed.path != '/' else 0,
152
- password=parsed.password
153
- )
154
- else:
155
- # 移除不支持的字段
156
- valid_fields = {'host', 'port', 'db', 'password'}
157
- filtered_config = {k: v for k, v in redis_config_data.items() if k in valid_fields}
158
- redis_config = RedisConfig(**filtered_config)
159
-
160
- print(f'{pg_config=}')
161
- print(f'{redis_config=}')
162
- # 创建消费者实例
163
- consumer = PostgreSQLConsumer(
164
- pg_config=pg_config,
165
- redis_config=redis_config,
166
- prefix=self.namespace_info.get('redis_prefix', 'jettask'),
167
- namespace_id=self.namespace_info.get('id'),
168
- namespace_name=self.namespace_info.get('name'),
169
- # consumer_strategy 参数已移除,现在只使用 HEARTBEAT 策略
170
- )
171
-
172
- logger.info(f"命名空间 {namespace_name} 的消费者实例创建成功,准备启动")
173
-
174
- # 启动消费者
175
- await consumer.start()
176
- logger.info(f"命名空间 {namespace_name} 的消费者已启动,进入运行状态")
177
-
178
- # 保持运行直到收到停止信号
179
- try:
180
- while not self.shutdown_event.is_set():
181
- await asyncio.sleep(1) # 每秒检查一次退出信号
182
- except asyncio.CancelledError:
183
- logger.info(f"命名空间 {namespace_name} 的消费者收到取消信号")
184
- finally:
185
- # 确保清理(带超时)
186
- logger.info(f"命名空间 {namespace_name} 开始执行优雅退出")
187
- try:
188
- # 设置 5 秒超时,避免因网络问题导致清理卡住
189
- await asyncio.wait_for(consumer.stop(), timeout=5.0)
190
- logger.info(f"命名空间 {namespace_name} 清理完成")
191
- except asyncio.TimeoutError:
192
- logger.warning(f"命名空间 {namespace_name} 清理超时(5秒),强制退出")
193
- except Exception as e:
194
- logger.error(f"命名空间 {namespace_name} 清理失败: {e}")
195
-
196
- except Exception as e:
197
- logger.error(f"命名空间 {namespace_name} 的消费者启动失败: {e}", exc_info=True)
198
- # 确保清理(带超时)
199
- if consumer:
200
- try:
201
- await asyncio.wait_for(consumer.stop(), timeout=5.0)
202
- except asyncio.TimeoutError:
203
- logger.warning(f"清理失败的消费者超时,跳过")
204
- except Exception as cleanup_error:
205
- logger.error(f"清理失败的消费者时出错: {cleanup_error}")
206
- raise
207
- finally:
208
- # 返回 consumer 实例以便在外层 finally 中使用
209
- return consumer
210
-
211
- def _signal_handler(self, signum, frame):
212
- """信号处理器 - 触发优雅退出"""
213
- logger.info(f"命名空间 {self.namespace_info['name']} 的消费进程收到信号 {signum}")
214
- # 设置退出事件,让主循环优雅退出
215
- self.shutdown_event.set()
216
-
217
-
218
- class MultiNamespaceConsumerManager:
219
- """多命名空间消费者管理器"""
220
-
221
- def __init__(self,
222
- task_center_url: str,
223
- namespace_check_interval: int = 60):
224
- """
225
- 初始化管理器
226
-
227
- Args:
228
- task_center_url: 任务中心的URL(必需,从中获取数据库配置)
229
- namespace_check_interval: 命名空间检测间隔(秒)
230
- """
231
- self.consumers: Dict[str, NamespaceConsumerProcess] = {}
232
- self.running = False
233
- self.task_center_url = task_center_url.rstrip('/')
234
- self.namespace_check_interval = namespace_check_interval
235
- self.task_center_db_url = None
236
-
237
- logger.info(f"命名空间检测间隔设置为: {self.namespace_check_interval} 秒")
238
- self.known_namespaces: Set[str] = set() # 已知的命名空间集合
239
-
240
- async def start(self, namespace_names: Optional[Set[str]] = None):
241
- """启动管理器
242
-
243
- Args:
244
- namespace_names: 要启动的命名空间名称集合,None表示启动所有命名空间
245
- """
246
- self.running = True
247
- logger.info("启动多命名空间消费者管理器")
248
-
249
- # 从任务中心获取数据库配置
250
- await self._init_database_config()
251
-
252
- # 获取命名空间配置
253
- namespaces = await self._fetch_namespaces(namespace_names)
254
-
255
- # 启动每个命名空间的消费者
256
- for ns_info in namespaces:
257
- try:
258
- self._start_namespace_consumer(ns_info)
259
- self.known_namespaces.add(ns_info['name'])
260
- except Exception as e:
261
- logger.error(f"启动命名空间 {ns_info['name']} 的消费者失败: {e}")
262
-
263
- # 创建并发任务:健康检查和命名空间检测
264
- try:
265
- health_check_task = asyncio.create_task(self._health_check_loop())
266
- namespace_check_task = asyncio.create_task(self._namespace_check_loop())
267
-
268
- # 等待任一任务完成(或出错)
269
- done, pending = await asyncio.wait(
270
- [health_check_task, namespace_check_task],
271
- return_when=asyncio.FIRST_EXCEPTION
272
- )
273
-
274
- # 取消所有未完成的任务
275
- for task in pending:
276
- task.cancel()
277
-
278
- except KeyboardInterrupt:
279
- logger.info("收到中断信号,停止管理器")
280
- finally:
281
- await self.stop()
282
-
283
- async def stop(self):
284
- """停止所有消费者"""
285
- self.running = False
286
- logger.info("停止所有命名空间消费者")
287
-
288
- for name, consumer in self.consumers.items():
289
- consumer.stop()
290
-
291
- self.consumers.clear()
292
-
293
- def _start_namespace_consumer(self, namespace_info: dict):
294
- """启动单个命名空间的消费者"""
295
- name = namespace_info['name']
296
-
297
- # 如果已存在,先停止
298
- if name in self.consumers:
299
- self.consumers[name].stop()
300
-
301
- # 创建并启动新进程
302
- consumer = NamespaceConsumerProcess(namespace_info)
303
- consumer.start()
304
- self.consumers[name] = consumer
305
-
306
- async def _init_database_config(self):
307
- """从任务中心获取数据库配置"""
308
- import aiohttp
309
- import json
310
-
311
- # 根据 URL 格式判断如何获取配置
312
- base_url = self.task_center_url
313
-
314
- # 如果是单命名空间URL,提取基础URL
315
- if '/api/namespaces/' in base_url:
316
- # 从 http://localhost:8001/api/namespaces/default 提取 http://localhost:8001
317
- base_url = base_url.split('/api/namespaces/')[0]
318
- elif '/api/v1/namespaces/' in base_url:
319
- # 从 http://localhost:8001/api/v1/namespaces/default 提取 http://localhost:8001
320
- base_url = base_url.split('/api/v1/namespaces/')[0]
321
-
322
- # 获取API服务的配置端点
323
- config_url = f"{base_url}/api/config"
324
-
325
- try:
326
- async with aiohttp.ClientSession() as session:
327
- async with session.get(config_url) as response:
328
- if response.status == 200:
329
- config = await response.json()
330
- # 从配置中提取数据库URL
331
- if 'database' in config:
332
- db_config = config['database']
333
- host = db_config.get('host', 'localhost')
334
- port = db_config.get('port', 5432)
335
- user = db_config.get('user', 'jettask')
336
- password = db_config.get('password', '123456')
337
- database = db_config.get('database', 'jettask')
338
- self.task_center_db_url = f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{database}"
339
- else:
340
- # 如果没有配置端点,使用默认值
341
- logger.warning(f"无法从任务中心获取数据库配置,使用默认配置")
342
- self.task_center_db_url = "postgresql+asyncpg://jettask:123456@localhost:5432/jettask"
343
- else:
344
- # API端点不存在,使用默认值
345
- logger.warning(f"任务中心配置端点返回错误: {response.status},使用默认配置")
346
- self.task_center_db_url = "postgresql+asyncpg://jettask:123456@localhost:5432/jettask"
347
- except Exception as e:
348
- logger.warning(f"无法连接到任务中心获取配置: {e},使用默认配置")
349
- # 使用默认配置
350
- self.task_center_db_url = "postgresql+asyncpg://jettask:123456@localhost:5432/jettask"
351
-
352
- logger.info(f"数据库配置已初始化")
353
-
354
- async def _fetch_namespaces(self, namespace_names: Optional[Set[str]] = None) -> list:
355
- """从任务中心数据库获取命名空间配置"""
356
- engine = create_async_engine(self.task_center_db_url, echo=False)
357
- AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
358
-
359
- namespaces = []
360
-
361
- async with AsyncSessionLocal() as session:
362
- try:
363
- # 查询命名空间
364
- if namespace_names:
365
- query = text("""
366
- SELECT id, name, redis_config, pg_config
367
- FROM namespaces
368
- WHERE name = ANY(:names)
369
- """)
370
- result = await session.execute(query, {'names': list(namespace_names)})
371
- else:
372
- query = text("""
373
- SELECT id, name, redis_config, pg_config
374
- FROM namespaces
375
- """)
376
- result = await session.execute(query)
377
-
378
- for row in result:
379
- # 直接使用 redis_config 和 pg_config
380
- redis_cfg = row.redis_config if row.redis_config else {}
381
- pg_cfg = row.pg_config if row.pg_config else {}
382
-
383
- # 处理 Redis 密码:如果为空字符串或 "None",设置为 None
384
- redis_password = redis_cfg.get('password')
385
- if redis_password in ['', 'None', 'null']:
386
- redis_password = None
387
-
388
- # 确定 Redis 前缀:
389
- # 1. 优先使用配置中的 prefix
390
- # 2. 其次使用命名空间名
391
- # 3. 最后使用 'jettask' 作为默认值
392
- redis_prefix = redis_cfg.get('prefix')
393
- if not redis_prefix:
394
- # 使用命名空间名作为前缀,这样不同命名空间的数据在 Redis 中是隔离的
395
- redis_prefix = row.name
396
-
397
- logger.info(f"命名空间 {row.name} 使用 Redis 前缀: {redis_prefix}")
398
-
399
- namespaces.append({
400
- 'id': row.id,
401
- 'name': row.name,
402
- 'redis_config': {
403
- 'host': redis_cfg.get('host', 'localhost'),
404
- 'port': redis_cfg.get('port', 6379),
405
- 'db': redis_cfg.get('db', 0),
406
- 'password': redis_password
407
- },
408
- 'pg_config': {
409
- 'host': pg_cfg.get('host', 'localhost'),
410
- 'port': pg_cfg.get('port', 5432),
411
- 'database': pg_cfg.get('database', 'jettask'),
412
- 'user': pg_cfg.get('user', 'jettask'),
413
- 'password': pg_cfg.get('password', '123456')
414
- },
415
- 'redis_prefix': redis_prefix
416
- })
417
-
418
- except Exception as e:
419
- logger.error(f"查询命名空间失败: {e}")
420
- # 如果查询失败,使用默认配置
421
- if not namespace_names or 'default' in namespace_names:
422
- namespaces.append(self._get_default_namespace())
423
-
424
- await engine.dispose()
425
- return namespaces
426
-
427
- def _parse_connection_config(self, connection_url: str) -> Optional[dict]:
428
- """解析连接配置字符串"""
429
- try:
430
- import json
431
- config = json.loads(connection_url)
432
-
433
- # 提取Redis配置
434
- redis_config = {}
435
- if 'redis' in config:
436
- redis_info = config['redis']
437
- redis_config = {
438
- 'host': redis_info.get('host', 'localhost'),
439
- 'port': redis_info.get('port', 6379),
440
- 'db': redis_info.get('db', 0),
441
- 'password': redis_info.get('password')
442
- }
443
-
444
- # 提取PostgreSQL配置
445
- pg_config = {}
446
- if 'postgres' in config:
447
- pg_info = config['postgres']
448
- pg_config = {
449
- 'host': pg_info.get('host', 'localhost'),
450
- 'port': pg_info.get('port', 5432),
451
- 'database': pg_info.get('database', 'jettask'),
452
- 'user': pg_info.get('user', 'jettask'),
453
- 'password': pg_info.get('password', '123456')
454
- }
455
-
456
- return {
457
- 'redis_config': redis_config,
458
- 'pg_config': pg_config,
459
- 'redis_prefix': config.get('prefix', 'jettask')
460
- }
461
-
462
- except Exception as e:
463
- logger.error(f"解析连接配置失败: {e}")
464
- return None
465
-
466
- def _get_default_namespace(self) -> dict:
467
- """获取默认命名空间配置"""
468
- return {
469
- 'id': 'default',
470
- 'name': 'default',
471
- 'redis_config': {
472
- 'host': os.environ.get('REDIS_HOST', 'localhost'),
473
- 'port': int(os.environ.get('REDIS_PORT', 6379)),
474
- 'db': int(os.environ.get('REDIS_DB', 0)),
475
- 'password': os.environ.get('REDIS_PASSWORD')
476
- },
477
- 'pg_config': {
478
- 'host': os.environ.get('POSTGRES_HOST', 'localhost'),
479
- 'port': int(os.environ.get('POSTGRES_PORT', 5432)),
480
- 'database': os.environ.get('POSTGRES_DB', 'jettask'),
481
- 'user': os.environ.get('POSTGRES_USER', 'jettask'),
482
- 'password': os.environ.get('POSTGRES_PASSWORD', '123456')
483
- },
484
- 'redis_prefix': os.environ.get('REDIS_PREFIX', 'default') # 使用 'default' 作为默认命名空间的前缀
485
- }
486
-
487
- async def _health_check_loop(self):
488
- """健康检查循环"""
489
- while self.running:
490
- try:
491
- await self._health_check()
492
- await asyncio.sleep(30) # 每30秒检查一次
493
- except Exception as e:
494
- logger.error(f"健康检查出错: {e}", exc_info=True)
495
- await asyncio.sleep(30)
496
-
497
- async def _namespace_check_loop(self):
498
- """命名空间检测循环"""
499
- # 首次启动后快速检测一次(5秒后)
500
- first_check_delay = min(5, self.namespace_check_interval)
501
- await asyncio.sleep(first_check_delay)
502
-
503
- while self.running:
504
- try:
505
- await self._check_new_namespaces()
506
- await asyncio.sleep(self.namespace_check_interval)
507
- except Exception as e:
508
- logger.error(f"命名空间检测出错: {e}", exc_info=True)
509
- await asyncio.sleep(self.namespace_check_interval)
510
-
511
- async def _check_new_namespaces(self):
512
- """检查是否有新的命名空间"""
513
- logger.debug("开始检查新命名空间...")
514
-
515
- # 获取所有活跃的命名空间
516
- current_namespaces = await self._fetch_namespaces()
517
- current_names = {ns['name'] for ns in current_namespaces}
518
-
519
- # 找出新增的命名空间
520
- new_names = current_names - self.known_namespaces
521
- if new_names:
522
- logger.info(f"发现新的命名空间: {new_names}")
523
-
524
- # 为新命名空间启动消费者
525
- for ns_info in current_namespaces:
526
- if ns_info['name'] in new_names:
527
- try:
528
- logger.info(f"为新命名空间 {ns_info['name']} 启动消费者")
529
- self._start_namespace_consumer(ns_info)
530
- self.known_namespaces.add(ns_info['name'])
531
- except Exception as e:
532
- logger.error(f"启动新命名空间 {ns_info['name']} 的消费者失败: {e}")
533
-
534
- # 找出被删除的命名空间
535
- removed_names = self.known_namespaces - current_names
536
- if removed_names:
537
- logger.info(f"发现被删除的命名空间: {removed_names}")
538
-
539
- # 停止被删除命名空间的消费者
540
- for name in removed_names:
541
- if name in self.consumers:
542
- logger.info(f"停止已删除命名空间 {name} 的消费者")
543
- self.consumers[name].stop()
544
- del self.consumers[name]
545
- self.known_namespaces.discard(name)
546
-
547
- async def _health_check(self):
548
- """检查消费进程健康状态"""
549
- for name, consumer in list(self.consumers.items()):
550
- if not consumer.is_alive():
551
- logger.warning(f"命名空间 {name} 的消费进程已停止,尝试重启")
552
-
553
- # 重新获取配置并重启
554
- namespaces = await self._fetch_namespaces({name})
555
- if namespaces:
556
- self._start_namespace_consumer(namespaces[0])
557
- else:
558
- logger.error(f"无法获取命名空间 {name} 的配置,移除该消费者")
559
- del self.consumers[name]
560
- self.known_namespaces.discard(name)
561
-
562
-
563
- async def main():
564
- """主函数"""
565
- import argparse
566
-
567
- parser = argparse.ArgumentParser(description='多命名空间PostgreSQL消费者')
568
- parser.add_argument(
569
- '--namespaces',
570
- nargs='*',
571
- help='要启动的命名空间列表,不指定则启动所有命名空间'
572
- )
573
-
574
- args = parser.parse_args()
575
-
576
- # 创建管理器
577
- manager = MultiNamespaceConsumerManager()
578
-
579
- # 设置信号处理
580
- def signal_handler(signum, frame):
581
- logger.info(f"收到信号 {signum},准备退出")
582
- asyncio.create_task(manager.stop())
583
-
584
- signal.signal(signal.SIGTERM, signal_handler)
585
- signal.signal(signal.SIGINT, signal_handler)
586
-
587
- # 启动管理器
588
- namespace_set = set(args.namespaces) if args.namespaces else None
589
- await manager.start(namespace_set)
590
-
591
-
592
- if __name__ == '__main__':
593
- asyncio.run(main())