mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 02:50:48 +08:00
Add biz_state monitoring with shared LLDP parse and batch collect.
Introduce ParseProfile-based business state tasks that reuse LLDP parsing for snapshot batches (separate from topology Fabric writes), with scheduler, export, and a Phase1 UI under network tasks. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
066158d2f7
commit
4fa1b9b4dc
27 changed files with 2487 additions and 1 deletions
138
netx_api/biz_state_scheduler.py
Normal file
138
netx_api/biz_state_scheduler.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""Background scheduler for biz_state collection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
|
||||
from .cli_budget import clamp_cli_workers
|
||||
from .config import settings
|
||||
from .db import SessionLocal
|
||||
from .models import BizStateTask
|
||||
from .biz_state.collect_runner import dispatch_collect
|
||||
|
||||
_log = logging.getLogger("netx.biz_state.scheduler")
|
||||
_stop = threading.Event()
|
||||
_thread: threading.Thread | None = None
|
||||
_dispatch_pool: ThreadPoolExecutor | None = None
|
||||
_pool_lock = threading.Lock()
|
||||
_last_tick_mono: float = 0.0
|
||||
|
||||
|
||||
def _utcnow() -> datetime:
|
||||
return datetime.utcnow()
|
||||
|
||||
|
||||
def _dispatch_pool_get() -> ThreadPoolExecutor:
|
||||
global _dispatch_pool
|
||||
with _pool_lock:
|
||||
if _dispatch_pool is None:
|
||||
workers = clamp_cli_workers(
|
||||
int(getattr(settings, "biz_state_dispatch_workers", 2) or 2),
|
||||
)
|
||||
_dispatch_pool = ThreadPoolExecutor(
|
||||
max_workers=workers, thread_name_prefix="biz-dispatch"
|
||||
)
|
||||
return _dispatch_pool
|
||||
|
||||
|
||||
def shutdown_biz_state_dispatch_pool(*, wait: bool = False) -> None:
|
||||
global _dispatch_pool
|
||||
with _pool_lock:
|
||||
if _dispatch_pool is not None:
|
||||
try:
|
||||
_dispatch_pool.shutdown(wait=wait, cancel_futures=True)
|
||||
except TypeError:
|
||||
_dispatch_pool.shutdown(wait=wait)
|
||||
_dispatch_pool = None
|
||||
|
||||
|
||||
def try_dispatch_due_tasks() -> int:
|
||||
import time as _time
|
||||
|
||||
global _last_tick_mono
|
||||
_last_tick_mono = _time.monotonic()
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
tasks = (
|
||||
db.query(BizStateTask)
|
||||
.filter(
|
||||
BizStateTask.status == "running",
|
||||
BizStateTask.collect_running.is_(False),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
due_ids: list[str] = []
|
||||
now = _utcnow()
|
||||
for task in tasks:
|
||||
interval = max(60, int(task.interval_sec or 300))
|
||||
ended = task.last_collect_ended_at
|
||||
if ended is None:
|
||||
due_ids.append(str(task.id))
|
||||
continue
|
||||
if (now - ended).total_seconds() >= interval:
|
||||
due_ids.append(str(task.id))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if not due_ids:
|
||||
return 0
|
||||
pool = _dispatch_pool_get()
|
||||
for tid in due_ids:
|
||||
try:
|
||||
pool.submit(dispatch_collect, tid)
|
||||
except Exception:
|
||||
_log.exception("biz_state submit failed task=%s", tid)
|
||||
return len(due_ids)
|
||||
|
||||
|
||||
def _loop() -> None:
|
||||
tick = max(5, int(getattr(settings, "biz_state_scheduler_tick_sec", 15) or 15))
|
||||
while not _stop.wait(tick):
|
||||
if not bool(getattr(settings, "biz_state_scheduler_enabled", True)):
|
||||
continue
|
||||
try:
|
||||
try_dispatch_due_tasks()
|
||||
except Exception:
|
||||
_log.exception("biz_state scheduler tick failed")
|
||||
|
||||
|
||||
def start_biz_state_scheduler() -> None:
|
||||
global _thread
|
||||
if not bool(getattr(settings, "biz_state_scheduler_enabled", True)):
|
||||
_log.info("biz_state scheduler disabled")
|
||||
return
|
||||
if _thread and _thread.is_alive():
|
||||
return
|
||||
_stop.clear()
|
||||
_thread = threading.Thread(target=_loop, name="biz-state-scheduler", daemon=True)
|
||||
_thread.start()
|
||||
_log.info("biz_state scheduler started")
|
||||
|
||||
|
||||
def stop_biz_state_scheduler() -> None:
|
||||
_stop.set()
|
||||
shutdown_biz_state_dispatch_pool(wait=False)
|
||||
global _thread
|
||||
t = _thread
|
||||
_thread = None
|
||||
if t and t.is_alive():
|
||||
t.join(timeout=2.0)
|
||||
_log.info("biz_state scheduler stopped")
|
||||
|
||||
|
||||
def biz_state_scheduler_status() -> dict:
|
||||
import time as _time
|
||||
|
||||
alive = bool(_thread and _thread.is_alive())
|
||||
age = None
|
||||
if _last_tick_mono:
|
||||
age = max(0.0, _time.monotonic() - _last_tick_mono)
|
||||
return {
|
||||
"running": alive,
|
||||
"enabled": bool(getattr(settings, "biz_state_scheduler_enabled", True)),
|
||||
"last_tick_age_sec": age,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue