jettask 0.2.20__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 +4 -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.20.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.20.dist-info/RECORD +0 -145
  107. {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/WHEEL +0 -0
  108. {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/entry_points.txt +0 -0
  109. {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/licenses/LICENSE +0 -0
  110. {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/top_level.txt +0 -0
@@ -1,516 +0,0 @@
1
- """
2
- 统一的数据库管理模块
3
- 提供命名空间级别的数据库连接管理
4
- """
5
- import os
6
- import logging
7
- import asyncio
8
- from typing import Dict, Optional, Any, AsyncGenerator
9
- from contextlib import asynccontextmanager
10
- from urllib.parse import urlparse
11
-
12
- import redis.asyncio as redis
13
- import asyncpg
14
- from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
15
- from sqlalchemy import text
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class NamespaceConfig:
21
- """命名空间配置"""
22
-
23
- def __init__(self, name: str, redis_config: dict, pg_config: dict):
24
- self.name = name
25
- self.redis_config = redis_config or {}
26
- self.pg_config = pg_config or {}
27
- self._parse_configs()
28
-
29
- def _parse_configs(self):
30
- """解析配置,提取URL和配置模式"""
31
- # Redis配置
32
- self.redis_mode = self.redis_config.get('config_mode', 'direct')
33
- self.redis_url = self.redis_config.get('url')
34
- self.redis_nacos_key = self.redis_config.get('nacos_key')
35
-
36
- # PostgreSQL配置
37
- self.pg_mode = self.pg_config.get('config_mode', 'direct')
38
- self.pg_url = self.pg_config.get('url')
39
- self.pg_nacos_key = self.pg_config.get('nacos_key')
40
-
41
- def has_redis(self) -> bool:
42
- """是否配置了Redis"""
43
- return bool(self.redis_url)
44
-
45
- def has_postgres(self) -> bool:
46
- """是否配置了PostgreSQL"""
47
- return bool(self.pg_url)
48
-
49
-
50
- class ConnectionPool:
51
- """单个命名空间的连接池"""
52
-
53
- def __init__(self, namespace: str, config: NamespaceConfig):
54
- self.namespace = namespace
55
- self.config = config
56
-
57
- # Redis连接池
58
- self._redis_client: Optional[redis.Redis] = None
59
- self._binary_redis_client: Optional[redis.Redis] = None
60
-
61
- # PostgreSQL连接池
62
- self._pg_pool: Optional[asyncpg.Pool] = None
63
-
64
- # SQLAlchemy引擎和会话工厂
65
- self._sa_engine: Optional[Any] = None
66
- self._sa_session_maker: Optional[async_sessionmaker] = None
67
-
68
- # 初始化锁
69
- self._init_lock = asyncio.Lock()
70
- self._initialized = False
71
-
72
- async def initialize(self):
73
- """初始化所有连接池"""
74
- async with self._init_lock:
75
- if self._initialized:
76
- return
77
-
78
- try:
79
- # 初始化Redis
80
- if self.config.has_redis():
81
- await self._init_redis()
82
-
83
- # 初始化PostgreSQL
84
- if self.config.has_postgres():
85
- await self._init_postgres()
86
-
87
- self._initialized = True
88
- logger.info(f"命名空间 '{self.namespace}' 连接池初始化成功")
89
-
90
- except Exception as e:
91
- logger.error(f"命名空间 '{self.namespace}' 连接池初始化失败: {e}")
92
- raise
93
-
94
- async def _init_redis(self):
95
- """初始化Redis客户端(使用全局客户端实例)"""
96
- from jettask.utils.db_connector import get_async_redis_client
97
-
98
- url = self.config.redis_url
99
-
100
- # 文本客户端(decode_responses=True)
101
- self._redis_client = get_async_redis_client(
102
- redis_url=url,
103
- max_connections=20,
104
- decode_responses=True
105
- )
106
-
107
- # 二进制客户端(decode_responses=False)
108
- self._binary_redis_client = get_async_redis_client(
109
- redis_url=url,
110
- max_connections=20,
111
- decode_responses=False
112
- )
113
-
114
- logger.debug(f"Redis客户端已就绪: {self.namespace}")
115
-
116
- async def _init_postgres(self):
117
- """初始化PostgreSQL连接池"""
118
- url = self.config.pg_url
119
-
120
- # 创建asyncpg连接池
121
- parsed = urlparse(url)
122
- self._pg_pool = await asyncpg.create_pool(
123
- host=parsed.hostname,
124
- port=parsed.port or 5432,
125
- user=parsed.username,
126
- password=parsed.password,
127
- database=parsed.path.lstrip('/'),
128
- min_size=2,
129
- max_size=10,
130
- command_timeout=30
131
- )
132
-
133
- # 创建SQLAlchemy引擎
134
- sa_url = url
135
- if sa_url.startswith('postgresql://'):
136
- sa_url = sa_url.replace('postgresql://', 'postgresql+asyncpg://', 1)
137
-
138
- self._sa_engine = create_async_engine(
139
- sa_url,
140
- pool_size=5,
141
- max_overflow=10,
142
- pool_timeout=30,
143
- pool_recycle=3600,
144
- pool_pre_ping=True,
145
- echo=False
146
- )
147
-
148
- self._sa_session_maker = async_sessionmaker(
149
- self._sa_engine,
150
- class_=AsyncSession,
151
- expire_on_commit=False
152
- )
153
-
154
- logger.debug(f"PostgreSQL连接池已创建: {self.namespace}")
155
-
156
- async def get_redis_client(self, decode: bool = True) -> redis.Redis:
157
- """获取Redis客户端"""
158
- if not self._initialized:
159
- await self.initialize()
160
-
161
- if not self.config.has_redis():
162
- raise ValueError(f"命名空间 '{self.namespace}' 未配置Redis")
163
-
164
- client = self._redis_client if decode else self._binary_redis_client
165
- return client
166
-
167
- @asynccontextmanager
168
- async def get_pg_connection(self) -> AsyncGenerator[asyncpg.Connection, None]:
169
- """获取PostgreSQL原生连接"""
170
- if not self._initialized:
171
- await self.initialize()
172
-
173
- if not self._pg_pool:
174
- raise ValueError(f"命名空间 '{self.namespace}' 未配置PostgreSQL")
175
-
176
- async with self._pg_pool.acquire() as conn:
177
- yield conn
178
-
179
- @asynccontextmanager
180
- async def get_sa_session(self) -> AsyncGenerator[AsyncSession, None]:
181
- """获取SQLAlchemy会话"""
182
- if not self._initialized:
183
- await self.initialize()
184
-
185
- if not self._sa_session_maker:
186
- raise ValueError(f"命名空间 '{self.namespace}' 未配置PostgreSQL")
187
-
188
- async with self._sa_session_maker() as session:
189
- try:
190
- yield session
191
- await session.commit()
192
- except Exception:
193
- await session.rollback()
194
- raise
195
- finally:
196
- await session.close()
197
-
198
- async def close(self):
199
- """关闭所有连接"""
200
- try:
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()
206
-
207
- if self._pg_pool:
208
- await self._pg_pool.close()
209
-
210
- if self._sa_engine:
211
- await self._sa_engine.dispose()
212
-
213
- self._initialized = False
214
- logger.info(f"命名空间 '{self.namespace}' 连接池已关闭")
215
-
216
- except Exception as e:
217
- logger.error(f"关闭命名空间 '{self.namespace}' 连接池失败: {e}")
218
-
219
-
220
- class UnifiedDatabaseManager:
221
- """
222
- 统一的数据库管理器
223
- 负责管理所有命名空间的数据库连接
224
- 支持从环境变量或Nacos读取配置
225
- """
226
-
227
- def __init__(self, use_nacos: bool = False):
228
- """
229
- 初始化数据库管理器
230
-
231
- Args:
232
- use_nacos: 是否从Nacos读取配置
233
- """
234
- # 连接池缓存
235
- self._pools: Dict[str, ConnectionPool] = {}
236
-
237
- # 配置缓存
238
- self._configs: Dict[str, NamespaceConfig] = {}
239
-
240
- # 是否使用Nacos配置
241
- self.use_nacos = use_nacos
242
-
243
- # 主数据库URL(用于读取命名空间配置)
244
- if use_nacos:
245
- # 从Nacos配置读取
246
- self._load_master_url_from_nacos()
247
- else:
248
- # 从环境变量读取
249
- self.master_pg_url = os.getenv(
250
- 'JETTASK_PG_URL',
251
- 'postgresql+asyncpg://jettask:123456@localhost:5432/jettask'
252
- )
253
-
254
- # 主数据库连接
255
- self._master_engine = None
256
- self._master_session_maker = None
257
-
258
- # 初始化锁
259
- self._init_lock = asyncio.Lock()
260
-
261
- # Nacos配置(如果需要)
262
- self._nacos_client = None
263
-
264
- def _load_master_url_from_nacos(self):
265
- """从Nacos配置加载主数据库URL"""
266
- try:
267
- from jettask.config.nacos_config import config
268
-
269
- # 从Nacos配置获取数据库连接信息
270
- pg_config = config.config
271
-
272
- # 构建数据库URL
273
- jettask_pg_url = pg_config.get('JETTASK_PG_URL')
274
- self.master_pg_url = jettask_pg_url
275
- logger.info(f"从Nacos加载数据库配置: {jettask_pg_url=}")
276
-
277
- except Exception as e:
278
- logger.error(f"从Nacos加载数据库配置失败: {e}")
279
- # 失败时使用默认值
280
- self.master_pg_url = os.getenv(
281
- 'JETTASK_PG_URL',
282
- 'postgresql+asyncpg://jettask:123456@localhost:5432/jettask'
283
- )
284
- logger.warning("使用默认数据库配置")
285
-
286
- async def initialize(self):
287
- """初始化管理器"""
288
- async with self._init_lock:
289
- if self._master_engine:
290
- return
291
- print(f'{self.master_pg_url=}')
292
- # 创建主数据库连接
293
- self._master_engine = create_async_engine(
294
- self.master_pg_url,
295
- pool_size=3,
296
- max_overflow=5,
297
- pool_pre_ping=True,
298
- echo=False
299
- )
300
-
301
- self._master_session_maker = async_sessionmaker(
302
- self._master_engine,
303
- class_=AsyncSession,
304
- expire_on_commit=False
305
- )
306
-
307
- logger.info("数据库管理器初始化完成")
308
-
309
- async def _fetch_namespace_config(self, namespace: str) -> NamespaceConfig:
310
- """从数据库获取命名空间配置"""
311
- if not self._master_session_maker:
312
- await self.initialize()
313
-
314
- async with self._master_session_maker() as session:
315
- query = text("""
316
- SELECT name, redis_config, pg_config, is_active
317
- FROM namespaces
318
- WHERE name = :name
319
- """)
320
-
321
- result = await session.execute(query, {'name': namespace})
322
- row = result.fetchone()
323
-
324
- if not row:
325
- raise ValueError(f"命名空间 '{namespace}' 不存在")
326
-
327
- if not row.is_active:
328
- raise ValueError(f"命名空间 '{namespace}' 未激活")
329
-
330
- # 处理Nacos配置
331
- redis_config = row.redis_config or {}
332
- pg_config = row.pg_config or {}
333
-
334
- # 如果是Nacos模式,需要从Nacos获取实际的URL
335
- if redis_config.get('config_mode') == 'nacos':
336
- redis_url = await self._get_from_nacos(redis_config.get('nacos_key'))
337
- redis_config['url'] = redis_url
338
-
339
- if pg_config.get('config_mode') == 'nacos':
340
- pg_url = await self._get_from_nacos(pg_config.get('nacos_key'))
341
- pg_config['url'] = pg_url
342
-
343
- return NamespaceConfig(row.name, redis_config, pg_config)
344
-
345
- async def _get_from_nacos(self, key: str) -> str:
346
- """从Nacos获取配置(需要实现)"""
347
- try:
348
- from jettask.config.nacos_config import Config
349
- if not self._nacos_client:
350
- self._nacos_client = Config()
351
- value = self._nacos_client.config.get(key)
352
- if not value:
353
- raise ValueError(f"Nacos配置键 '{key}' 不存在")
354
- return value
355
- except ImportError:
356
- logger.warning("Nacos客户端未安装,返回占位URL")
357
- return f"redis://nacos-placeholder/{key}"
358
-
359
- async def get_pool(self, namespace: str) -> ConnectionPool:
360
- """获取命名空间的连接池"""
361
- # 检查缓存
362
- if namespace in self._pools:
363
- return self._pools[namespace]
364
-
365
- # 从数据库获取配置(而不是通过HTTP)
366
- if namespace not in self._configs:
367
- config = await self._fetch_namespace_config(namespace)
368
- self._configs[namespace] = config
369
-
370
- # 创建新的连接池
371
- pool = ConnectionPool(namespace, self._configs[namespace])
372
- await pool.initialize()
373
-
374
- # 缓存连接池
375
- self._pools[namespace] = pool
376
-
377
- return pool
378
-
379
- async def get_redis_client(self, namespace: str, decode: bool = True) -> redis.Redis:
380
- """便捷方法:获取Redis客户端"""
381
- pool = await self.get_pool(namespace)
382
- return await pool.get_redis_client(decode)
383
-
384
- @asynccontextmanager
385
- async def get_pg_connection(self, namespace: str) -> AsyncGenerator[asyncpg.Connection, None]:
386
- """便捷方法:获取PostgreSQL连接"""
387
- pool = await self.get_pool(namespace)
388
- async with pool.get_pg_connection() as conn:
389
- yield conn
390
-
391
- @asynccontextmanager
392
- async def get_session(self, namespace: str) -> AsyncGenerator[AsyncSession, None]:
393
- """便捷方法:获取SQLAlchemy会话"""
394
- pool = await self.get_pool(namespace)
395
- async with pool.get_sa_session() as session:
396
- yield session
397
-
398
- @asynccontextmanager
399
- async def get_master_session(self) -> AsyncGenerator[AsyncSession, None]:
400
- """获取主数据库会话(用于管理命名空间)"""
401
- if not self._master_session_maker:
402
- await self.initialize()
403
-
404
- async with self._master_session_maker() as session:
405
- try:
406
- yield session
407
- await session.commit()
408
- except Exception:
409
- await session.rollback()
410
- raise
411
- finally:
412
- await session.close()
413
-
414
- async def refresh_config(self, namespace: str):
415
- """刷新命名空间配置"""
416
- # 关闭旧连接
417
- if namespace in self._pools:
418
- await self._pools[namespace].close()
419
- del self._pools[namespace]
420
-
421
- # 清除配置缓存
422
- if namespace in self._configs:
423
- del self._configs[namespace]
424
-
425
- logger.info(f"已刷新命名空间 '{namespace}' 的配置")
426
-
427
- async def list_namespaces(self) -> list:
428
- """列出所有命名空间"""
429
- async with self.get_master_session() as session:
430
- query = text("""
431
- SELECT name, description, is_active, created_at, updated_at
432
- FROM namespaces
433
- ORDER BY name
434
- """)
435
-
436
- result = await session.execute(query)
437
- rows = result.fetchall()
438
-
439
- return [
440
- {
441
- 'name': row.name,
442
- 'description': row.description,
443
- 'is_active': row.is_active,
444
- 'created_at': row.created_at.isoformat() if row.created_at else None,
445
- 'updated_at': row.updated_at.isoformat() if row.updated_at else None
446
- }
447
- for row in rows
448
- ]
449
-
450
- async def close_all(self):
451
- """关闭所有连接"""
452
- # 关闭所有命名空间连接池
453
- for namespace, pool in list(self._pools.items()):
454
- await pool.close()
455
-
456
- self._pools.clear()
457
- self._configs.clear()
458
-
459
- # 关闭主数据库连接
460
- if self._master_engine:
461
- await self._master_engine.dispose()
462
-
463
- logger.info("数据库管理器已关闭所有连接")
464
-
465
- async def __aenter__(self):
466
- """异步上下文管理器入口"""
467
- await self.initialize()
468
- return self
469
-
470
- async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None):
471
- """异步上下文管理器出口"""
472
- await self.close_all()
473
-
474
-
475
- # 全局实例
476
- _db_manager: Optional[UnifiedDatabaseManager] = None
477
-
478
-
479
- def get_db_manager(use_nacos: bool = None) -> UnifiedDatabaseManager:
480
- """
481
- 获取全局数据库管理器实例
482
-
483
- Args:
484
- use_nacos: 是否使用Nacos配置,None表示使用已有实例的设置
485
- """
486
- global _db_manager
487
- if _db_manager is None:
488
- # 如果没有指定,检查环境变量或默认值
489
- if use_nacos is None:
490
- use_nacos = os.getenv('USE_NACOS', 'false').lower() == 'true'
491
- _db_manager = UnifiedDatabaseManager(use_nacos=use_nacos)
492
- elif use_nacos is not None and _db_manager.use_nacos != use_nacos:
493
- # 如果配置模式改变了,重新创建实例
494
- logger.info(f"配置模式改变,重新创建数据库管理器 (use_nacos={use_nacos})")
495
- _db_manager = UnifiedDatabaseManager(use_nacos=use_nacos)
496
- return _db_manager
497
-
498
-
499
- async def init_db_manager(use_nacos: bool = None):
500
- """
501
- 初始化全局数据库管理器
502
-
503
- Args:
504
- use_nacos: 是否使用Nacos配置
505
- """
506
- manager = get_db_manager(use_nacos=use_nacos)
507
- await manager.initialize()
508
- return manager
509
-
510
-
511
- async def close_db_manager():
512
- """关闭全局数据库管理器"""
513
- global _db_manager
514
- if _db_manager:
515
- await _db_manager.close_all()
516
- _db_manager = None
@@ -1,81 +0,0 @@
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)