feat: deliver alert events through persistent outbox
This commit is contained in:
@@ -1,94 +1,142 @@
|
||||
"""
|
||||
定时任务调度器
|
||||
|
||||
使用 asyncio 循环驱动 Ping 引擎,协调 Pinger 和 Alerter。
|
||||
"""
|
||||
"""Single-task scheduler for probe, event recording, and outbox delivery."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Callable
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.services.pinger import Pinger
|
||||
from app.services.alerter import Alerter
|
||||
from app.config import Settings, settings
|
||||
from app.core.deps import async_session
|
||||
from app.services.alerter import Alerter
|
||||
from app.services.notification_dispatcher import (
|
||||
NotificationDispatcher,
|
||||
WeComClient,
|
||||
)
|
||||
from app.services.pinger import Pinger
|
||||
|
||||
|
||||
logger = logging.getLogger("pingwatch.scheduler")
|
||||
_DEFAULT_DISPATCH_INTERVAL_SECONDS = 5.0
|
||||
|
||||
|
||||
class PingScheduler:
|
||||
"""
|
||||
调度器职责:
|
||||
1. 按间隔驱动 Ping 引擎
|
||||
2. 每轮结束后触发 Alerter 处理待发送告警
|
||||
3. 控制并发和清理
|
||||
"""
|
||||
"""Serialize monitoring cycles and retain one cancellable background task."""
|
||||
|
||||
def __init__(self):
|
||||
self._pinger = Pinger()
|
||||
self._alerter = Alerter()
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
pinger=None,
|
||||
alerter=None,
|
||||
dispatcher=None,
|
||||
session_factory: Callable = async_session,
|
||||
runtime_settings: Settings | None = None,
|
||||
interval_seconds: float | None = None,
|
||||
dispatch_interval_seconds: float = _DEFAULT_DISPATCH_INTERVAL_SECONDS,
|
||||
):
|
||||
self._settings = runtime_settings or settings
|
||||
self._pinger = pinger or Pinger(self._settings)
|
||||
self._alerter = alerter or Alerter(self._settings)
|
||||
self._notification_client = None
|
||||
self._dispatch_enabled = (
|
||||
dispatcher is not None or self._settings.wecom_notification_enabled
|
||||
)
|
||||
if dispatcher is None:
|
||||
self._notification_client = WeComClient(self._settings)
|
||||
dispatcher = NotificationDispatcher(
|
||||
self._notification_client,
|
||||
self._settings,
|
||||
)
|
||||
self._dispatcher = dispatcher
|
||||
self._session_factory = session_factory
|
||||
self._probe_interval_seconds = max(
|
||||
1.0,
|
||||
float(
|
||||
interval_seconds
|
||||
if interval_seconds is not None
|
||||
else self._settings.PING_INTERVAL_SECONDS
|
||||
),
|
||||
)
|
||||
self._dispatch_interval_seconds = min(
|
||||
max(1.0, float(dispatch_interval_seconds)),
|
||||
30.0,
|
||||
)
|
||||
self._running = False
|
||||
self._task: asyncio.Task | None = None
|
||||
self._cycle_lock = asyncio.Lock()
|
||||
|
||||
# 注册状态变化回调
|
||||
self._pinger.on_state_change(self._on_state_change)
|
||||
async def _run_cycle(self, *, run_probe: bool = True) -> None:
|
||||
"""Run one serialized transaction cycle and close its session on cancel."""
|
||||
async with self._cycle_lock:
|
||||
async with self._session_factory() as db:
|
||||
try:
|
||||
if run_probe:
|
||||
changes = await self._pinger.run_one_round(db)
|
||||
for change in changes:
|
||||
await self._alerter.record_transition(change, db)
|
||||
await db.commit()
|
||||
|
||||
async def _on_state_change(self, change):
|
||||
"""收到设备状态变化,转给 alerter"""
|
||||
async with async_session() as db:
|
||||
try:
|
||||
await self._alerter.on_state_change(change, db)
|
||||
except Exception as e:
|
||||
logger.error(f"告警处理异常: {e}", exc_info=True)
|
||||
|
||||
async def _run_loop(self):
|
||||
"""主循环"""
|
||||
logger.info("Ping 调度器已启动")
|
||||
self._running = True
|
||||
if self._dispatch_enabled:
|
||||
await self._dispatcher.dispatch_due(db, datetime.now())
|
||||
await db.commit()
|
||||
except BaseException:
|
||||
await db.rollback()
|
||||
raise
|
||||
|
||||
async def _run_loop(self) -> None:
|
||||
"""Probe at its configured cadence and service retries every few seconds."""
|
||||
logger.info("Ping scheduler started")
|
||||
next_probe_at = 0.0
|
||||
while self._running:
|
||||
now = time.monotonic()
|
||||
run_probe = now >= next_probe_at
|
||||
try:
|
||||
async with async_session() as db:
|
||||
# 执行一轮 ping
|
||||
results = await self._pinger.run_one_round(db)
|
||||
|
||||
if results:
|
||||
# 直接用启用的设备数
|
||||
total = len(results)
|
||||
|
||||
# 处理待发送告警
|
||||
await self._alerter.flush_pending(db, total)
|
||||
|
||||
await self._run_cycle(run_probe=run_probe)
|
||||
if run_probe:
|
||||
next_probe_at = time.monotonic() + self._probe_interval_seconds
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"调度器异常: {e}", exc_info=True)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Scheduler cycle failed type=%s",
|
||||
type(exc).__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
if run_probe:
|
||||
next_probe_at = time.monotonic() + self._probe_interval_seconds
|
||||
|
||||
# 等待下一轮
|
||||
await asyncio.sleep(settings.PING_INTERVAL_SECONDS)
|
||||
seconds_until_probe = max(0.0, next_probe_at - time.monotonic())
|
||||
sleep_seconds = min(
|
||||
self._dispatch_interval_seconds,
|
||||
seconds_until_probe or self._dispatch_interval_seconds,
|
||||
)
|
||||
try:
|
||||
await asyncio.sleep(sleep_seconds)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
logger.info("Ping scheduler stopped")
|
||||
|
||||
logger.info("Ping 调度器已停止")
|
||||
|
||||
def start(self):
|
||||
"""启动调度器(后台任务)"""
|
||||
if self._running:
|
||||
logger.warning("调度器已在运行")
|
||||
def start(self) -> None:
|
||||
"""Start exactly one background scheduler task."""
|
||||
if self._task is not None and not self._task.done():
|
||||
logger.warning("Ping scheduler is already running")
|
||||
return
|
||||
self._running = True
|
||||
self._task = asyncio.create_task(self._run_loop())
|
||||
|
||||
async def stop(self):
|
||||
"""停止调度器"""
|
||||
async def stop(self) -> None:
|
||||
"""Cancel and await the active cycle before releasing its HTTP client."""
|
||||
self._running = False
|
||||
if self._task:
|
||||
if self._task is not None:
|
||||
self._task.cancel()
|
||||
try:
|
||||
await self._task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._task = None
|
||||
if self._notification_client is not None:
|
||||
await self._notification_client.aclose()
|
||||
self._notification_client = None
|
||||
|
||||
|
||||
# 全局调度器实例
|
||||
scheduler = PingScheduler()
|
||||
|
||||
Reference in New Issue
Block a user