netx/netx_api/config_sync_recovery.py
oliver 0eead0e662 Add industrial config sync separate from collection tasks.
Periodic vendor-aware CLI sync stores zlib snapshots in Postgres with dashboard, retry, and config viewer under Network Management.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 10:25:36 +08:00

68 lines
2.1 KiB
Python

"""Startup recovery for interrupted config-sync cycles."""
from __future__ import annotations
import logging
from sqlalchemy.orm import Session
from .config_sync_runner import dispatch_cycle
from .config_sync_service import finalize_cycle, sync_cycle_progress
from .models import ConfigSyncCycle, ConfigSyncTask
from datetime import datetime
_log = logging.getLogger("netx.config_sync.recovery")
def recover_config_sync_on_startup(db: Session) -> int:
"""
Mark orphaned running tasks as fail(orphan_recovered), then resume pending
tasks for cycles still marked running/paused.
"""
cycles = (
db.query(ConfigSyncCycle)
.filter(ConfigSyncCycle.status.in_(("running", "paused", "pending")))
.all()
)
resumed = 0
for cycle in cycles:
cycle_id = str(cycle.id)
orphans = (
db.query(ConfigSyncTask)
.filter(ConfigSyncTask.cycle_id == cycle_id, ConfigSyncTask.status == "running")
.all()
)
for task in orphans:
task.status = "fail"
task.message = "orphan_recovered"
task.ended_at = datetime.utcnow()
if orphans:
db.commit()
_log.info("config_sync recovery cycle=%s orphaned_tasks=%s", cycle_id, len(orphans))
sync_cycle_progress(db, cycle_id)
db.refresh(cycle)
if str(cycle.status) == "paused":
continue
pending = (
db.query(ConfigSyncTask)
.filter(ConfigSyncTask.cycle_id == cycle_id, ConfigSyncTask.status == "pending")
.count()
)
if pending <= 0:
if str(cycle.status) in ("running", "pending"):
finalize_cycle(db, cycle_id)
continue
if str(cycle.status) == "pending":
cycle.status = "running"
if not cycle.started_at:
cycle.started_at = datetime.utcnow()
db.commit()
n = dispatch_cycle(cycle_id)
resumed += n
_log.info("config_sync recovery resumed cycle=%s pending=%s", cycle_id, n)
return resumed