jettask 0.2.18__py3-none-any.whl → 0.2.20__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 +60 -2
- jettask/cli.py +314 -228
- jettask/config/__init__.py +9 -1
- jettask/config/config.py +245 -0
- jettask/config/env_loader.py +381 -0
- jettask/config/lua_scripts.py +158 -0
- jettask/config/nacos_config.py +132 -5
- jettask/core/__init__.py +1 -1
- jettask/core/app.py +1573 -666
- jettask/core/app_importer.py +33 -16
- jettask/core/container.py +532 -0
- jettask/core/task.py +1 -4
- jettask/core/unified_manager_base.py +2 -2
- jettask/executor/__init__.py +38 -0
- jettask/executor/core.py +625 -0
- jettask/executor/executor.py +338 -0
- jettask/executor/orchestrator.py +290 -0
- jettask/executor/process_entry.py +638 -0
- jettask/executor/task_executor.py +317 -0
- jettask/messaging/__init__.py +68 -0
- jettask/messaging/event_pool.py +2188 -0
- jettask/messaging/reader.py +519 -0
- jettask/messaging/registry.py +266 -0
- jettask/messaging/scanner.py +369 -0
- jettask/messaging/sender.py +312 -0
- jettask/persistence/__init__.py +118 -0
- jettask/persistence/backlog_monitor.py +567 -0
- jettask/{backend/data_access.py → persistence/base.py} +58 -57
- jettask/persistence/consumer.py +315 -0
- jettask/{core → persistence}/db_manager.py +23 -22
- jettask/persistence/maintenance.py +81 -0
- jettask/persistence/message_consumer.py +259 -0
- jettask/{backend/namespace_data_access.py → persistence/namespace.py} +66 -98
- jettask/persistence/offline_recovery.py +196 -0
- jettask/persistence/queue_discovery.py +215 -0
- jettask/persistence/task_persistence.py +218 -0
- jettask/persistence/task_updater.py +583 -0
- jettask/scheduler/__init__.py +2 -2
- jettask/scheduler/loader.py +6 -5
- jettask/scheduler/run_scheduler.py +1 -1
- jettask/scheduler/scheduler.py +7 -7
- jettask/scheduler/{unified_scheduler_manager.py → scheduler_coordinator.py} +18 -13
- jettask/task/__init__.py +16 -0
- jettask/{router.py → task/router.py} +26 -8
- jettask/task/task_center/__init__.py +9 -0
- jettask/task/task_executor.py +318 -0
- jettask/task/task_registry.py +291 -0
- jettask/test_connection_monitor.py +73 -0
- jettask/utils/__init__.py +31 -1
- jettask/{monitor/run_backlog_collector.py → utils/backlog_collector.py} +1 -1
- jettask/utils/db_connector.py +1629 -0
- jettask/{db_init.py → utils/db_init.py} +1 -1
- jettask/utils/rate_limit/__init__.py +30 -0
- jettask/utils/rate_limit/concurrency_limiter.py +665 -0
- jettask/utils/rate_limit/config.py +145 -0
- jettask/utils/rate_limit/limiter.py +41 -0
- jettask/utils/rate_limit/manager.py +269 -0
- jettask/utils/rate_limit/qps_limiter.py +154 -0
- jettask/utils/rate_limit/task_limiter.py +384 -0
- jettask/utils/serializer.py +3 -0
- jettask/{monitor/stream_backlog_monitor.py → utils/stream_backlog.py} +14 -6
- jettask/utils/time_sync.py +173 -0
- jettask/webui/__init__.py +27 -0
- jettask/{api/v1 → webui/api}/alerts.py +1 -1
- jettask/{api/v1 → webui/api}/analytics.py +2 -2
- jettask/{api/v1 → webui/api}/namespaces.py +1 -1
- jettask/{api/v1 → webui/api}/overview.py +1 -1
- jettask/{api/v1 → webui/api}/queues.py +3 -3
- jettask/{api/v1 → webui/api}/scheduled.py +1 -1
- jettask/{api/v1 → webui/api}/settings.py +1 -1
- jettask/{api.py → webui/app.py} +253 -145
- jettask/webui/namespace_manager/__init__.py +10 -0
- jettask/{multi_namespace_consumer.py → webui/namespace_manager/multi.py} +69 -22
- jettask/{unified_consumer_manager.py → webui/namespace_manager/unified.py} +1 -1
- jettask/{run.py → webui/run.py} +2 -2
- jettask/{services → webui/services}/__init__.py +1 -3
- jettask/{services → webui/services}/overview_service.py +34 -16
- jettask/{services → webui/services}/queue_service.py +1 -1
- jettask/{backend → webui/services}/queue_stats_v2.py +1 -1
- jettask/{services → webui/services}/settings_service.py +1 -1
- jettask/worker/__init__.py +53 -0
- jettask/worker/lifecycle.py +1507 -0
- jettask/worker/manager.py +583 -0
- jettask/{core/offline_worker_recovery.py → worker/recovery.py} +268 -175
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/METADATA +2 -71
- jettask-0.2.20.dist-info/RECORD +145 -0
- jettask/__main__.py +0 -140
- jettask/api/__init__.py +0 -103
- jettask/backend/__init__.py +0 -1
- jettask/backend/api/__init__.py +0 -3
- jettask/backend/api/v1/__init__.py +0 -17
- jettask/backend/api/v1/monitoring.py +0 -431
- jettask/backend/api/v1/namespaces.py +0 -504
- jettask/backend/api/v1/queues.py +0 -342
- jettask/backend/api/v1/tasks.py +0 -367
- jettask/backend/core/__init__.py +0 -3
- jettask/backend/core/cache.py +0 -221
- jettask/backend/core/database.py +0 -200
- jettask/backend/core/exceptions.py +0 -102
- jettask/backend/dependencies.py +0 -261
- jettask/backend/init_meta_db.py +0 -158
- jettask/backend/main.py +0 -1426
- jettask/backend/main_unified.py +0 -78
- jettask/backend/main_v2.py +0 -394
- jettask/backend/models/__init__.py +0 -3
- jettask/backend/models/requests.py +0 -236
- jettask/backend/models/responses.py +0 -230
- jettask/backend/namespace_api_old.py +0 -267
- jettask/backend/services/__init__.py +0 -3
- jettask/backend/start.py +0 -42
- jettask/backend/unified_api_router.py +0 -1541
- jettask/cleanup_deprecated_tables.sql +0 -16
- jettask/core/consumer_manager.py +0 -1695
- jettask/core/delay_scanner.py +0 -256
- jettask/core/event_pool.py +0 -1700
- jettask/core/heartbeat_process.py +0 -222
- jettask/core/task_batch.py +0 -153
- jettask/core/worker_scanner.py +0 -271
- jettask/executors/__init__.py +0 -5
- jettask/executors/asyncio.py +0 -876
- jettask/executors/base.py +0 -30
- jettask/executors/common.py +0 -148
- jettask/executors/multi_asyncio.py +0 -309
- jettask/gradio_app.py +0 -570
- jettask/integrated_gradio_app.py +0 -1088
- jettask/main.py +0 -0
- jettask/monitoring/__init__.py +0 -3
- jettask/pg_consumer.py +0 -1896
- jettask/run_monitor.py +0 -22
- jettask/run_webui.py +0 -148
- jettask/scheduler/multi_namespace_scheduler.py +0 -294
- jettask/scheduler/unified_manager.py +0 -450
- jettask/task_center_client.py +0 -150
- jettask/utils/serializer_optimized.py +0 -33
- jettask/webui_exceptions.py +0 -67
- jettask-0.2.18.dist-info/RECORD +0 -150
- /jettask/{constants.py → config/constants.py} +0 -0
- /jettask/{backend/config.py → config/task_center.py} +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/pg_consumer_v2.py +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/add_execution_time_field.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_new_tables.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_tables_v3.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/migrate_to_new_structure.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/modify_time_fields.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql_utils.py +0 -0
- /jettask/{models.py → persistence/models.py} +0 -0
- /jettask/scheduler/{manager.py → task_crud.py} +0 -0
- /jettask/{schema.sql → schemas/schema.sql} +0 -0
- /jettask/{task_center.py → task/task_center/client.py} +0 -0
- /jettask/{monitoring → utils}/file_watcher.py +0 -0
- /jettask/{services/redis_monitor_service.py → utils/redis_monitor.py} +0 -0
- /jettask/{api/v1 → webui/api}/__init__.py +0 -0
- /jettask/{webui_config.py → webui/config.py} +0 -0
- /jettask/{webui_models → webui/models}/__init__.py +0 -0
- /jettask/{webui_models → webui/models}/namespace.py +0 -0
- /jettask/{services → webui/services}/alert_service.py +0 -0
- /jettask/{services → webui/services}/analytics_service.py +0 -0
- /jettask/{services → webui/services}/scheduled_task_service.py +0 -0
- /jettask/{services → webui/services}/task_service.py +0 -0
- /jettask/{webui_sql → webui/sql}/batch_upsert_functions.sql +0 -0
- /jettask/{webui_sql → webui/sql}/verify_database.sql +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/top_level.txt +0 -0
@@ -10,8 +10,13 @@ from datetime import datetime, timedelta, timezone
|
|
10
10
|
from typing import Dict, List, Optional, Tuple
|
11
11
|
import redis.asyncio as redis
|
12
12
|
from sqlalchemy import text, bindparam
|
13
|
-
from sqlalchemy.ext.asyncio import
|
14
|
-
|
13
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
14
|
+
|
15
|
+
# 导入统一的数据库连接工具
|
16
|
+
from ..utils.db_connector import (
|
17
|
+
get_dual_mode_async_redis_client,
|
18
|
+
get_pg_engine_and_factory
|
19
|
+
)
|
15
20
|
|
16
21
|
# 设置日志
|
17
22
|
logger = logging.getLogger(__name__)
|
@@ -62,67 +67,60 @@ class PostgreSQLConfig:
|
|
62
67
|
|
63
68
|
|
64
69
|
class JetTaskDataAccess:
|
65
|
-
"""JetTask
|
66
|
-
|
70
|
+
"""JetTask数据访问类(使用统一的数据库连接工具)"""
|
71
|
+
|
67
72
|
def __init__(self):
|
68
73
|
self.redis_config = RedisConfig.from_env()
|
69
74
|
self.pg_config = PostgreSQLConfig.from_env()
|
70
75
|
# Redis前缀可以从环境变量配置,默认为 "jettask"
|
71
76
|
self.redis_prefix = os.environ.get('JETTASK_REDIS_PREFIX', 'jettask')
|
77
|
+
|
78
|
+
# 使用全局单例连接池
|
79
|
+
self._text_redis_client: Optional[redis.Redis] = None
|
80
|
+
self._binary_redis_client: Optional[redis.Redis] = None
|
81
|
+
|
82
|
+
# PostgreSQL 相关
|
72
83
|
self.async_engine = None
|
73
84
|
self.AsyncSessionLocal = None
|
74
|
-
self._redis_pool = None
|
75
|
-
self._binary_redis_pool = None # 用于处理包含二进制数据的Stream
|
76
85
|
|
77
86
|
async def initialize(self):
|
78
|
-
"""
|
87
|
+
"""初始化数据库连接(使用全局单例)"""
|
79
88
|
try:
|
80
|
-
#
|
81
|
-
|
82
|
-
if
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
89
|
+
# 构建 Redis URL
|
90
|
+
redis_url = f"redis://"
|
91
|
+
if self.redis_config.password:
|
92
|
+
redis_url += f":{self.redis_config.password}@"
|
93
|
+
redis_url += f"{self.redis_config.host}:{self.redis_config.port}/{self.redis_config.db}"
|
94
|
+
|
95
|
+
# 构建 PostgreSQL 配置
|
96
|
+
pg_config = {
|
97
|
+
'host': self.pg_config.host,
|
98
|
+
'port': self.pg_config.port,
|
99
|
+
'user': self.pg_config.user,
|
100
|
+
'password': self.pg_config.password,
|
101
|
+
'database': self.pg_config.database
|
102
|
+
}
|
103
|
+
|
104
|
+
# 初始化 PostgreSQL 连接(使用全局单例)
|
105
|
+
self.async_engine, self.AsyncSessionLocal = get_pg_engine_and_factory(
|
106
|
+
config=pg_config,
|
87
107
|
pool_size=10,
|
88
108
|
max_overflow=5,
|
89
109
|
pool_pre_ping=True,
|
90
110
|
echo=False
|
91
111
|
)
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
)
|
98
|
-
|
99
|
-
# 初始化Redis连接池(用于普通操作)
|
100
|
-
self._redis_pool = redis.ConnectionPool(
|
101
|
-
host=self.redis_config.host,
|
102
|
-
port=self.redis_config.port,
|
103
|
-
db=self.redis_config.db,
|
104
|
-
password=self.redis_config.password,
|
105
|
-
encoding='utf-8',
|
106
|
-
decode_responses=True,
|
107
|
-
socket_keepalive=True,
|
108
|
-
socket_connect_timeout=5,
|
109
|
-
retry_on_timeout=True
|
110
|
-
)
|
111
|
-
|
112
|
-
# 初始化二进制Redis连接池(用于Stream操作)
|
113
|
-
self._binary_redis_pool = redis.ConnectionPool(
|
114
|
-
host=self.redis_config.host,
|
115
|
-
port=self.redis_config.port,
|
116
|
-
db=self.redis_config.db,
|
117
|
-
password=self.redis_config.password,
|
118
|
-
decode_responses=False, # 不解码,因为Stream包含msgpack二进制数据
|
112
|
+
|
113
|
+
# 初始化 Redis 连接(使用全局单例,双模式)
|
114
|
+
self._text_redis_client, self._binary_redis_client = get_dual_mode_async_redis_client(
|
115
|
+
redis_url=redis_url,
|
116
|
+
max_connections=50,
|
119
117
|
socket_keepalive=True,
|
120
118
|
socket_connect_timeout=5,
|
121
119
|
retry_on_timeout=True
|
122
120
|
)
|
123
|
-
|
121
|
+
|
124
122
|
logger.info("数据库连接初始化成功")
|
125
|
-
|
123
|
+
|
126
124
|
except Exception as e:
|
127
125
|
logger.error(f"数据库连接初始化失败: {e}")
|
128
126
|
raise
|
@@ -132,21 +130,24 @@ class JetTaskDataAccess:
|
|
132
130
|
return self.AsyncSessionLocal()
|
133
131
|
|
134
132
|
async def close(self):
|
135
|
-
"""
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
133
|
+
"""关闭数据库连接(由于使用全局单例,这里只重置状态)"""
|
134
|
+
# 注意:连接池由全局单例管理,这里只清理引用
|
135
|
+
self._text_redis_client = None
|
136
|
+
self._binary_redis_client = None
|
137
|
+
self.async_engine = None
|
138
|
+
self.AsyncSessionLocal = None
|
139
|
+
|
143
140
|
async def get_redis_client(self):
|
144
|
-
"""获取Redis
|
145
|
-
|
146
|
-
|
141
|
+
"""获取 Redis 客户端(使用全局单例)"""
|
142
|
+
if not self._text_redis_client:
|
143
|
+
raise RuntimeError("Redis client not initialized")
|
144
|
+
return self._text_redis_client
|
145
|
+
|
147
146
|
async def get_binary_redis_client(self):
|
148
|
-
"""获取二进制Redis客户端(用于Stream
|
149
|
-
|
147
|
+
"""获取二进制 Redis 客户端(用于Stream操作,使用全局单例)"""
|
148
|
+
if not self._binary_redis_client:
|
149
|
+
raise RuntimeError("Binary Redis client not initialized")
|
150
|
+
return self._binary_redis_client
|
150
151
|
|
151
152
|
async def fetch_queues_data(self) -> List[Dict]:
|
152
153
|
"""获取队列数据(基于Redis Stream)"""
|
@@ -2046,7 +2047,7 @@ class JetTaskDataAccess:
|
|
2046
2047
|
async def _sync_task_to_redis(self, task_id: str, enabled: bool):
|
2047
2048
|
"""同步任务状态到 Redis"""
|
2048
2049
|
try:
|
2049
|
-
if not self.
|
2050
|
+
if not self._redis_connector:
|
2050
2051
|
logger.debug("Redis not configured, skipping sync")
|
2051
2052
|
return
|
2052
2053
|
|
@@ -0,0 +1,315 @@
|
|
1
|
+
"""PostgreSQL消费者主模块
|
2
|
+
|
3
|
+
协调各个子模块,提供统一的消费者接口。
|
4
|
+
"""
|
5
|
+
|
6
|
+
import asyncio
|
7
|
+
import logging
|
8
|
+
import os
|
9
|
+
import socket
|
10
|
+
import traceback
|
11
|
+
from typing import Optional
|
12
|
+
|
13
|
+
import redis.asyncio as redis
|
14
|
+
from redis.asyncio import Redis
|
15
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
16
|
+
from sqlalchemy.orm import sessionmaker
|
17
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
18
|
+
from sqlalchemy import text
|
19
|
+
|
20
|
+
from jettask.webui.config import PostgreSQLConfig, RedisConfig
|
21
|
+
from jettask.worker.manager import ConsumerManager
|
22
|
+
|
23
|
+
from .backlog_monitor import BacklogMonitor
|
24
|
+
from .task_updater import TaskUpdater
|
25
|
+
from .offline_recovery import OfflineRecoveryHandler
|
26
|
+
from .task_persistence import TaskPersistence
|
27
|
+
from .queue_discovery import QueueDiscovery
|
28
|
+
from .message_consumer import MessageConsumer
|
29
|
+
from .maintenance import DatabaseMaintenance
|
30
|
+
|
31
|
+
logger = logging.getLogger(__name__)
|
32
|
+
|
33
|
+
|
34
|
+
class PostgreSQLConsumer:
|
35
|
+
"""PostgreSQL消费者,从Redis队列消费任务并持久化到PostgreSQL
|
36
|
+
|
37
|
+
支持多租户(命名空间)隔离
|
38
|
+
|
39
|
+
架构说明:
|
40
|
+
- BacklogMonitor: 监控Stream积压情况
|
41
|
+
- TaskUpdater: 更新任务状态(从TASK_CHANGES流)
|
42
|
+
- OfflineRecoveryHandler: 恢复离线worker的消息
|
43
|
+
- TaskPersistence: 解析并持久化任务数据
|
44
|
+
- QueueDiscovery: 发现和管理队列
|
45
|
+
- MessageConsumer: 消费队列消息
|
46
|
+
- DatabaseMaintenance: 数据库维护任务
|
47
|
+
"""
|
48
|
+
|
49
|
+
def __init__(
|
50
|
+
self,
|
51
|
+
pg_config: PostgreSQLConfig,
|
52
|
+
redis_config: RedisConfig,
|
53
|
+
prefix: str = "jettask",
|
54
|
+
node_id: str = None,
|
55
|
+
# consumer_strategy 参数已移除,现在只使用 HEARTBEAT 策略
|
56
|
+
namespace_id: str = None,
|
57
|
+
namespace_name: str = None,
|
58
|
+
enable_backlog_monitor: bool = True,
|
59
|
+
backlog_monitor_interval: int = 1
|
60
|
+
):
|
61
|
+
"""初始化PostgreSQL消费者
|
62
|
+
|
63
|
+
Args:
|
64
|
+
pg_config: PostgreSQL配置
|
65
|
+
redis_config: Redis配置
|
66
|
+
prefix: Redis键前缀
|
67
|
+
node_id: 节点ID
|
68
|
+
consumer_strategy: 消费者策略
|
69
|
+
namespace_id: 命名空间ID
|
70
|
+
namespace_name: 命名空间名称
|
71
|
+
enable_backlog_monitor: 是否启用积压监控
|
72
|
+
backlog_monitor_interval: 积压监控间隔(秒)
|
73
|
+
"""
|
74
|
+
self.pg_config = pg_config
|
75
|
+
self.redis_config = redis_config
|
76
|
+
self.prefix = prefix
|
77
|
+
|
78
|
+
# 命名空间支持
|
79
|
+
self.namespace_id = namespace_id
|
80
|
+
self.namespace_name = namespace_name or "default"
|
81
|
+
|
82
|
+
# 节点标识
|
83
|
+
hostname = socket.gethostname()
|
84
|
+
self.node_id = node_id or f"{hostname}_{os.getpid()}"
|
85
|
+
|
86
|
+
# 消费者配置
|
87
|
+
# consumer_strategy 已移除,现在只使用 HEARTBEAT 策略
|
88
|
+
self.consumer_group = f"{prefix}_pg_consumer"
|
89
|
+
|
90
|
+
# Redis和数据库连接(将在start时初始化)
|
91
|
+
self.redis_client: Optional[Redis] = None
|
92
|
+
self.async_engine = None
|
93
|
+
self.AsyncSessionLocal = None
|
94
|
+
|
95
|
+
# ConsumerManager(将在start时初始化)
|
96
|
+
self.consumer_manager = None
|
97
|
+
self.consumer_id = None
|
98
|
+
|
99
|
+
# 各个子模块(将在start时初始化)
|
100
|
+
self.backlog_monitor: Optional[BacklogMonitor] = None
|
101
|
+
self.task_updater: Optional[TaskUpdater] = None
|
102
|
+
self.offline_recovery: Optional[OfflineRecoveryHandler] = None
|
103
|
+
self.task_persistence: Optional[TaskPersistence] = None
|
104
|
+
self.queue_discovery: Optional[QueueDiscovery] = None
|
105
|
+
self.message_consumer: Optional[MessageConsumer] = None
|
106
|
+
self.database_maintenance: Optional[DatabaseMaintenance] = None
|
107
|
+
|
108
|
+
# 积压监控配置
|
109
|
+
self.enable_backlog_monitor = enable_backlog_monitor
|
110
|
+
self.backlog_monitor_interval = backlog_monitor_interval
|
111
|
+
|
112
|
+
self._running = False
|
113
|
+
|
114
|
+
async def start(self):
|
115
|
+
"""启动消费者"""
|
116
|
+
logger.info(f"Starting PostgreSQL consumer (modular) on node: {self.node_id}")
|
117
|
+
|
118
|
+
# 1. 连接Redis(使用全局客户端实例)
|
119
|
+
from jettask.utils.db_connector import get_async_redis_client, get_sync_redis_client
|
120
|
+
|
121
|
+
# 构建 Redis URL
|
122
|
+
redis_url = f"redis://"
|
123
|
+
if self.redis_config.password:
|
124
|
+
redis_url += f":{self.redis_config.password}@"
|
125
|
+
redis_url += f"{self.redis_config.host}:{self.redis_config.port}/{self.redis_config.db}"
|
126
|
+
|
127
|
+
self.redis_client = get_async_redis_client(
|
128
|
+
redis_url=redis_url,
|
129
|
+
decode_responses=False # 保持二进制模式
|
130
|
+
)
|
131
|
+
|
132
|
+
# 2. 初始化ConsumerManager(需要同步的Redis客户端)
|
133
|
+
sync_redis_client = get_sync_redis_client(
|
134
|
+
redis_url=redis_url,
|
135
|
+
decode_responses=True
|
136
|
+
)
|
137
|
+
|
138
|
+
# 配置ConsumerManager
|
139
|
+
initial_queues = ['TASK_CHANGES'] # TASK_CHANGES是固定的
|
140
|
+
consumer_config = {
|
141
|
+
'redis_prefix': self.prefix,
|
142
|
+
'queues': initial_queues,
|
143
|
+
'worker_prefix': 'PG_CONSUMER', # 使用不同的前缀,与task worker区分开
|
144
|
+
}
|
145
|
+
|
146
|
+
self.consumer_manager = ConsumerManager(
|
147
|
+
redis_client=sync_redis_client,
|
148
|
+
# strategy 参数已移除,现在只使用 HEARTBEAT 策略
|
149
|
+
config=consumer_config
|
150
|
+
)
|
151
|
+
|
152
|
+
# 获取稳定的consumer_id
|
153
|
+
self.consumer_id = self.consumer_manager.get_consumer_name('TASK_CHANGES')
|
154
|
+
logger.debug(f"Using consumer_id: {self.consumer_id} with strategy: HEARTBEAT")
|
155
|
+
|
156
|
+
# 3. 创建SQLAlchemy异步引擎
|
157
|
+
if self.pg_config.dsn.startswith('postgresql://'):
|
158
|
+
dsn = self.pg_config.dsn.replace('postgresql://', 'postgresql+asyncpg://', 1)
|
159
|
+
else:
|
160
|
+
dsn = self.pg_config.dsn
|
161
|
+
|
162
|
+
self.async_engine = create_async_engine(
|
163
|
+
dsn,
|
164
|
+
pool_size=50,
|
165
|
+
max_overflow=20,
|
166
|
+
pool_pre_ping=True,
|
167
|
+
pool_recycle=300,
|
168
|
+
echo=False
|
169
|
+
)
|
170
|
+
|
171
|
+
# 预热连接池
|
172
|
+
logger.debug("Pre-warming database connection pool...")
|
173
|
+
async with self.async_engine.begin() as conn:
|
174
|
+
await conn.execute(text("SELECT 1"))
|
175
|
+
|
176
|
+
# 创建异步会话工厂
|
177
|
+
self.AsyncSessionLocal = sessionmaker(
|
178
|
+
self.async_engine,
|
179
|
+
class_=AsyncSession,
|
180
|
+
expire_on_commit=False
|
181
|
+
)
|
182
|
+
|
183
|
+
# 4. 初始化各个子模块
|
184
|
+
# 任务持久化处理器
|
185
|
+
self.task_persistence = TaskPersistence(
|
186
|
+
async_session_local=self.AsyncSessionLocal,
|
187
|
+
namespace_id=self.namespace_id,
|
188
|
+
namespace_name=self.namespace_name
|
189
|
+
)
|
190
|
+
|
191
|
+
# 队列发现器
|
192
|
+
self.queue_discovery = QueueDiscovery(
|
193
|
+
redis_client=self.redis_client,
|
194
|
+
redis_prefix=self.prefix,
|
195
|
+
consumer_group=self.consumer_group,
|
196
|
+
consumer_manager=self.consumer_manager
|
197
|
+
)
|
198
|
+
|
199
|
+
# 先进行一次队列发现,确保ConsumerManager有正确的队列列表
|
200
|
+
await self.queue_discovery.initial_queue_discovery()
|
201
|
+
|
202
|
+
# 消息消费器
|
203
|
+
self.message_consumer = MessageConsumer(
|
204
|
+
redis_client=self.redis_client,
|
205
|
+
redis_prefix=self.prefix,
|
206
|
+
consumer_group=self.consumer_group,
|
207
|
+
consumer_id=self.consumer_id,
|
208
|
+
task_persistence=self.task_persistence,
|
209
|
+
queue_discovery=self.queue_discovery
|
210
|
+
)
|
211
|
+
|
212
|
+
# 任务状态更新器
|
213
|
+
self.task_updater = TaskUpdater(
|
214
|
+
redis_client=self.redis_client,
|
215
|
+
async_session_local=self.AsyncSessionLocal,
|
216
|
+
redis_prefix=self.prefix,
|
217
|
+
consumer_id=self.consumer_id
|
218
|
+
)
|
219
|
+
|
220
|
+
# 离线恢复处理器
|
221
|
+
self.offline_recovery = OfflineRecoveryHandler(
|
222
|
+
redis_client=self.redis_client,
|
223
|
+
redis_prefix=self.prefix,
|
224
|
+
consumer_id=self.consumer_id,
|
225
|
+
task_updater=self.task_updater
|
226
|
+
)
|
227
|
+
# 延迟初始化(需要consumer_manager)
|
228
|
+
self.offline_recovery.set_consumer_manager(self.consumer_manager)
|
229
|
+
|
230
|
+
# 数据库维护
|
231
|
+
self.database_maintenance = DatabaseMaintenance(
|
232
|
+
async_session_local=self.AsyncSessionLocal
|
233
|
+
)
|
234
|
+
|
235
|
+
# 积压监控器
|
236
|
+
self.backlog_monitor = BacklogMonitor(
|
237
|
+
redis_client=self.redis_client,
|
238
|
+
async_session_local=self.AsyncSessionLocal,
|
239
|
+
redis_prefix=self.prefix,
|
240
|
+
namespace_name=self.namespace_name,
|
241
|
+
node_id=self.node_id,
|
242
|
+
enable_monitor=self.enable_backlog_monitor,
|
243
|
+
monitor_interval=self.backlog_monitor_interval
|
244
|
+
)
|
245
|
+
|
246
|
+
# 5. 启动所有子模块
|
247
|
+
self._running = True
|
248
|
+
|
249
|
+
# 启动队列发现
|
250
|
+
await self.queue_discovery.start_discovery()
|
251
|
+
|
252
|
+
# 启动消息消费
|
253
|
+
await self.message_consumer.start()
|
254
|
+
|
255
|
+
# 启动任务状态更新
|
256
|
+
await self.task_updater.start()
|
257
|
+
|
258
|
+
# 启动离线恢复
|
259
|
+
await self.offline_recovery.start()
|
260
|
+
|
261
|
+
# 启动数据库维护
|
262
|
+
await self.database_maintenance.start()
|
263
|
+
|
264
|
+
# 启动积压监控
|
265
|
+
if self.enable_backlog_monitor:
|
266
|
+
await self.backlog_monitor.start()
|
267
|
+
logger.info(f"Stream backlog monitor enabled with {self.backlog_monitor_interval}s interval")
|
268
|
+
|
269
|
+
# 如果使用HEARTBEAT策略,ConsumerManager会自动管理心跳
|
270
|
+
if self.consumer_manager:
|
271
|
+
logger.debug("Heartbeat is managed by ConsumerManager")
|
272
|
+
|
273
|
+
logger.debug("PostgreSQL consumer started successfully")
|
274
|
+
|
275
|
+
async def stop(self):
|
276
|
+
"""停止消费者"""
|
277
|
+
logger.debug("Stopping PostgreSQL consumer...")
|
278
|
+
self._running = False
|
279
|
+
|
280
|
+
# 停止所有子模块
|
281
|
+
if self.backlog_monitor:
|
282
|
+
await self.backlog_monitor.stop()
|
283
|
+
|
284
|
+
if self.database_maintenance:
|
285
|
+
await self.database_maintenance.stop()
|
286
|
+
|
287
|
+
if self.offline_recovery:
|
288
|
+
await self.offline_recovery.stop()
|
289
|
+
|
290
|
+
if self.task_updater:
|
291
|
+
await self.task_updater.stop()
|
292
|
+
|
293
|
+
if self.message_consumer:
|
294
|
+
await self.message_consumer.stop()
|
295
|
+
|
296
|
+
if self.queue_discovery:
|
297
|
+
await self.queue_discovery.stop_discovery()
|
298
|
+
|
299
|
+
# 清理ConsumerManager
|
300
|
+
if self.consumer_manager:
|
301
|
+
try:
|
302
|
+
self.consumer_manager.cleanup()
|
303
|
+
logger.debug(f"Cleaned up ConsumerManager for consumer: {self.consumer_id}")
|
304
|
+
except Exception as e:
|
305
|
+
logger.error(f"Error cleaning up ConsumerManager: {e}")
|
306
|
+
logger.error(traceback.format_exc())
|
307
|
+
|
308
|
+
# 关闭连接
|
309
|
+
if self.redis_client:
|
310
|
+
await self.redis_client.close()
|
311
|
+
|
312
|
+
if self.async_engine:
|
313
|
+
await self.async_engine.dispose()
|
314
|
+
|
315
|
+
logger.debug("PostgreSQL consumer stopped")
|
@@ -55,8 +55,8 @@ class ConnectionPool:
|
|
55
55
|
self.config = config
|
56
56
|
|
57
57
|
# Redis连接池
|
58
|
-
self.
|
59
|
-
self.
|
58
|
+
self._redis_client: Optional[redis.Redis] = None
|
59
|
+
self._binary_redis_client: Optional[redis.Redis] = None
|
60
60
|
|
61
61
|
# PostgreSQL连接池
|
62
62
|
self._pg_pool: Optional[asyncpg.Pool] = None
|
@@ -92,25 +92,26 @@ class ConnectionPool:
|
|
92
92
|
raise
|
93
93
|
|
94
94
|
async def _init_redis(self):
|
95
|
-
"""初始化Redis
|
95
|
+
"""初始化Redis客户端(使用全局客户端实例)"""
|
96
|
+
from jettask.utils.db_connector import get_async_redis_client
|
97
|
+
|
96
98
|
url = self.config.redis_url
|
97
|
-
|
98
|
-
#
|
99
|
-
self.
|
100
|
-
url,
|
99
|
+
|
100
|
+
# 文本客户端(decode_responses=True)
|
101
|
+
self._redis_client = get_async_redis_client(
|
102
|
+
redis_url=url,
|
101
103
|
max_connections=20,
|
102
|
-
decode_responses=True
|
103
|
-
encoding='utf-8'
|
104
|
+
decode_responses=True
|
104
105
|
)
|
105
|
-
|
106
|
-
#
|
107
|
-
self.
|
108
|
-
url,
|
106
|
+
|
107
|
+
# 二进制客户端(decode_responses=False)
|
108
|
+
self._binary_redis_client = get_async_redis_client(
|
109
|
+
redis_url=url,
|
109
110
|
max_connections=20,
|
110
111
|
decode_responses=False
|
111
112
|
)
|
112
|
-
|
113
|
-
logger.debug(f"Redis
|
113
|
+
|
114
|
+
logger.debug(f"Redis客户端已就绪: {self.namespace}")
|
114
115
|
|
115
116
|
async def _init_postgres(self):
|
116
117
|
"""初始化PostgreSQL连接池"""
|
@@ -160,8 +161,8 @@ class ConnectionPool:
|
|
160
161
|
if not self.config.has_redis():
|
161
162
|
raise ValueError(f"命名空间 '{self.namespace}' 未配置Redis")
|
162
163
|
|
163
|
-
|
164
|
-
return
|
164
|
+
client = self._redis_client if decode else self._binary_redis_client
|
165
|
+
return client
|
165
166
|
|
166
167
|
@asynccontextmanager
|
167
168
|
async def get_pg_connection(self) -> AsyncGenerator[asyncpg.Connection, None]:
|
@@ -197,11 +198,11 @@ class ConnectionPool:
|
|
197
198
|
async def close(self):
|
198
199
|
"""关闭所有连接"""
|
199
200
|
try:
|
200
|
-
if self.
|
201
|
-
await self.
|
202
|
-
|
203
|
-
if self.
|
204
|
-
await self.
|
201
|
+
if self._redis_client:
|
202
|
+
await self._redis_client.aclose()
|
203
|
+
|
204
|
+
if self._binary_redis_client:
|
205
|
+
await self._binary_redis_client.aclose()
|
205
206
|
|
206
207
|
if self._pg_pool:
|
207
208
|
await self._pg_pool.close()
|
@@ -0,0 +1,81 @@
|
|
1
|
+
"""数据库维护模块
|
2
|
+
|
3
|
+
负责定期执行数据库维护任务,如ANALYZE等。
|
4
|
+
"""
|
5
|
+
|
6
|
+
import asyncio
|
7
|
+
import logging
|
8
|
+
import time
|
9
|
+
import traceback
|
10
|
+
|
11
|
+
from sqlalchemy import text
|
12
|
+
from sqlalchemy.orm import sessionmaker
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class DatabaseMaintenance:
|
18
|
+
"""数据库维护处理器
|
19
|
+
|
20
|
+
职责:
|
21
|
+
- 定期执行ANALYZE命令更新统计信息
|
22
|
+
- 优化查询性能
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self, async_session_local: sessionmaker):
|
26
|
+
"""初始化数据库维护处理器
|
27
|
+
|
28
|
+
Args:
|
29
|
+
async_session_local: SQLAlchemy会话工厂
|
30
|
+
"""
|
31
|
+
self.AsyncSessionLocal = async_session_local
|
32
|
+
|
33
|
+
# 维护配置
|
34
|
+
self.analyze_interval = 7200 # 每2小时执行一次ANALYZE
|
35
|
+
self.last_analyze_time = 0
|
36
|
+
|
37
|
+
self._running = False
|
38
|
+
self._maintenance_task = None
|
39
|
+
|
40
|
+
async def start(self):
|
41
|
+
"""启动维护任务"""
|
42
|
+
self._running = True
|
43
|
+
self._maintenance_task = asyncio.create_task(self._maintenance_loop())
|
44
|
+
logger.debug("DatabaseMaintenance started")
|
45
|
+
|
46
|
+
async def stop(self):
|
47
|
+
"""停止维护任务"""
|
48
|
+
self._running = False
|
49
|
+
|
50
|
+
if self._maintenance_task:
|
51
|
+
self._maintenance_task.cancel()
|
52
|
+
try:
|
53
|
+
await self._maintenance_task
|
54
|
+
except asyncio.CancelledError:
|
55
|
+
pass
|
56
|
+
|
57
|
+
logger.debug("DatabaseMaintenance stopped")
|
58
|
+
|
59
|
+
async def _maintenance_loop(self):
|
60
|
+
"""维护循环"""
|
61
|
+
while self._running:
|
62
|
+
try:
|
63
|
+
current_time = time.time()
|
64
|
+
|
65
|
+
# 执行ANALYZE
|
66
|
+
if current_time - self.last_analyze_time > self.analyze_interval:
|
67
|
+
async with self.AsyncSessionLocal() as session:
|
68
|
+
logger.debug("Running ANALYZE on tasks and task_runs tables...")
|
69
|
+
await session.execute(text("ANALYZE tasks"))
|
70
|
+
await session.execute(text("ANALYZE task_runs"))
|
71
|
+
await session.commit()
|
72
|
+
logger.debug("ANALYZE completed successfully for both tables")
|
73
|
+
self.last_analyze_time = current_time
|
74
|
+
|
75
|
+
# 每5分钟检查一次
|
76
|
+
await asyncio.sleep(300)
|
77
|
+
|
78
|
+
except Exception as e:
|
79
|
+
logger.error(f"Error in database maintenance: {e}")
|
80
|
+
logger.error(traceback.format_exc())
|
81
|
+
await asyncio.sleep(60)
|