08a0157c95
调度页执行周期可视化/单次执行/分类选机/推送源对齐;登录 IP 门控与无缝导航; 服务器批量后台任务、执行记录、凭据合并、各页激活刷新与错误提示修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
3.5 KiB
Python
142 lines
3.5 KiB
Python
"""Server batch job live state in Redis (2h TTL)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Optional
|
|
|
|
from server.infrastructure.redis.client import get_redis
|
|
|
|
logger = logging.getLogger("nexus.server_batch_store")
|
|
|
|
REDIS_KEY_PREFIX = "server_batch:"
|
|
REDIS_NEXT_ID = "server_batch:next_id"
|
|
REDIS_TTL_SECONDS = 7200
|
|
TERMINAL_STATUSES = frozenset({"completed", "failed", "partial", "cancelled"})
|
|
|
|
|
|
def _now_iso() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
async def next_job_id() -> int:
|
|
redis = get_redis()
|
|
return int(await redis.incr(REDIS_NEXT_ID))
|
|
|
|
|
|
def _key(job_id: int) -> str:
|
|
return f"{REDIS_KEY_PREFIX}{job_id}"
|
|
|
|
|
|
def progress_stats(live: dict[str, Any]) -> dict[str, Any]:
|
|
server_ids = live.get("server_ids") or []
|
|
results = live.get("results") or {}
|
|
total = len(server_ids)
|
|
done = 0
|
|
success = 0
|
|
failed = 0
|
|
for sid in server_ids:
|
|
entry = results.get(str(sid))
|
|
if not entry:
|
|
continue
|
|
done += 1
|
|
if entry.get("success"):
|
|
success += 1
|
|
else:
|
|
failed += 1
|
|
remaining = max(0, total - done)
|
|
return {
|
|
"total": total,
|
|
"done": done,
|
|
"remaining": remaining,
|
|
"success": success,
|
|
"failed": failed,
|
|
"progress": f"{success}/{total}" if total else "—",
|
|
}
|
|
|
|
|
|
async def save_live(live: dict[str, Any]) -> None:
|
|
redis = get_redis()
|
|
job_id = int(live["id"])
|
|
live["updated_at"] = _now_iso()
|
|
await redis.set(_key(job_id), json.dumps(live, ensure_ascii=False), ex=REDIS_TTL_SECONDS)
|
|
|
|
|
|
async def get_live(job_id: int) -> Optional[dict[str, Any]]:
|
|
redis = get_redis()
|
|
raw = await redis.get(_key(job_id))
|
|
if not raw:
|
|
return None
|
|
try:
|
|
return json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
logger.warning("Corrupt server batch live state job_id=%s", job_id)
|
|
return None
|
|
|
|
|
|
def init_live(
|
|
*,
|
|
job_id: int,
|
|
op: str,
|
|
label: str,
|
|
server_ids: list[int],
|
|
operator: str,
|
|
params: Optional[dict[str, Any]] = None,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"id": job_id,
|
|
"op": op,
|
|
"label": label,
|
|
"status": "running",
|
|
"server_ids": list(server_ids),
|
|
"results": {},
|
|
"operator": operator,
|
|
"params": params or {},
|
|
"created_at": _now_iso(),
|
|
"updated_at": _now_iso(),
|
|
}
|
|
|
|
|
|
def record_result(live: dict[str, Any], item: dict[str, Any]) -> None:
|
|
results = live.setdefault("results", {})
|
|
results[str(item["server_id"])] = item
|
|
|
|
|
|
def finalize_status(live: dict[str, Any]) -> str:
|
|
stats = progress_stats(live)
|
|
total = stats["total"]
|
|
success = stats["success"]
|
|
failed = stats["failed"]
|
|
if total == 0:
|
|
status = "failed"
|
|
elif failed == 0:
|
|
status = "completed"
|
|
elif success == 0:
|
|
status = "failed"
|
|
else:
|
|
status = "partial"
|
|
live["status"] = status
|
|
return status
|
|
|
|
|
|
def live_to_api(live: dict[str, Any]) -> dict[str, Any]:
|
|
stats = progress_stats(live)
|
|
results = live.get("results") or {}
|
|
server_ids = live.get("server_ids") or []
|
|
ordered = []
|
|
for sid in server_ids:
|
|
entry = results.get(str(sid))
|
|
if entry:
|
|
ordered.append(entry)
|
|
return {
|
|
"job_id": int(live["id"]),
|
|
"op": live.get("op") or "",
|
|
"label": live.get("label") or "",
|
|
"status": live.get("status") or "running",
|
|
"operator": live.get("operator") or "",
|
|
**stats,
|
|
"results": ordered,
|
|
}
|