Files
Nexus/server/infrastructure/redis/server_batch_store.py
T
Nexus Agent 08a0157c95 feat: 调度表单重构、登录门控、批量任务与页面缓存对齐
调度页执行周期可视化/单次执行/分类选机/推送源对齐;登录 IP 门控与无缝导航;
服务器批量后台任务、执行记录、凭据合并、各页激活刷新与错误提示修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 11:17:21 +08:00

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,
}