69068e2e39
部署对齐三项后端能力:启动/周期收尾僵尸批量任务、30 天推送日志 purge、已安装且版本不低于主站时跳过批量安装 Agent。 Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
4.2 KiB
Python
135 lines
4.2 KiB
Python
"""Server batch job persistence (MySQL)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Optional
|
|
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from server.domain.models import ServerBatchJob
|
|
|
|
|
|
class ServerBatchJobRepositoryImpl:
|
|
def __init__(self, session: AsyncSession):
|
|
self.session = session
|
|
|
|
async def get_by_id(self, job_id: int) -> Optional[ServerBatchJob]:
|
|
result = await self.session.execute(
|
|
select(ServerBatchJob).where(ServerBatchJob.id == job_id)
|
|
)
|
|
return result.scalar_one_or_none()
|
|
|
|
async def create(self, job: ServerBatchJob) -> ServerBatchJob:
|
|
self.session.add(job)
|
|
await self.session.commit()
|
|
return job
|
|
|
|
async def upsert_running(
|
|
self,
|
|
*,
|
|
job_id: int,
|
|
op: str,
|
|
label: str,
|
|
server_ids: list[int],
|
|
operator: str,
|
|
params: Optional[dict[str, Any]] = None,
|
|
) -> ServerBatchJob:
|
|
row = await self.get_by_id(job_id)
|
|
if row:
|
|
return row
|
|
job = ServerBatchJob(
|
|
id=job_id,
|
|
op=op,
|
|
label=label,
|
|
server_ids=json.dumps(server_ids, ensure_ascii=False),
|
|
status="running",
|
|
results=json.dumps([], ensure_ascii=False),
|
|
operator=operator,
|
|
params=json.dumps(params or {}, ensure_ascii=False),
|
|
)
|
|
return await self.create(job)
|
|
|
|
async def finalize(
|
|
self,
|
|
job_id: int,
|
|
*,
|
|
status: str,
|
|
results: list[dict[str, Any]],
|
|
) -> Optional[ServerBatchJob]:
|
|
row = await self.get_by_id(job_id)
|
|
if not row:
|
|
return None
|
|
row.status = status
|
|
row.results = json.dumps(results, ensure_ascii=False)
|
|
row.completed_at = datetime.now(timezone.utc)
|
|
await self.session.commit()
|
|
return row
|
|
|
|
async def list_running(self) -> list[ServerBatchJob]:
|
|
result = await self.session.execute(
|
|
select(ServerBatchJob).where(ServerBatchJob.status == "running")
|
|
)
|
|
return list(result.scalars().all())
|
|
|
|
async def list_recent(
|
|
self,
|
|
*,
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
op: Optional[str] = None,
|
|
) -> tuple[list[ServerBatchJob], int]:
|
|
filters = []
|
|
if op:
|
|
filters.append(ServerBatchJob.op == op)
|
|
count_stmt = select(func.count(ServerBatchJob.id))
|
|
if filters:
|
|
count_stmt = count_stmt.where(*filters)
|
|
total = int((await self.session.execute(count_stmt)).scalar() or 0)
|
|
stmt = select(ServerBatchJob).order_by(ServerBatchJob.started_at.desc())
|
|
if filters:
|
|
stmt = stmt.where(*filters)
|
|
result = await self.session.execute(stmt.limit(limit).offset(offset))
|
|
return list(result.scalars().all()), total
|
|
|
|
|
|
def job_row_to_api(row: ServerBatchJob) -> dict[str, Any]:
|
|
"""Convert MySQL row to BatchJobStatus-shaped dict."""
|
|
try:
|
|
server_ids = json.loads(row.server_ids or "[]")
|
|
except json.JSONDecodeError:
|
|
server_ids = []
|
|
try:
|
|
results = json.loads(row.results or "[]")
|
|
except json.JSONDecodeError:
|
|
results = []
|
|
if isinstance(results, dict):
|
|
ordered = []
|
|
for sid in server_ids:
|
|
entry = results.get(str(sid))
|
|
if entry:
|
|
ordered.append(entry)
|
|
results = ordered
|
|
total = len(server_ids)
|
|
success = sum(1 for r in results if r.get("success"))
|
|
failed = sum(1 for r in results if not r.get("success"))
|
|
done = len(results)
|
|
return {
|
|
"job_id": int(row.id),
|
|
"op": row.op or "",
|
|
"label": row.label or "",
|
|
"status": row.status or "completed",
|
|
"operator": row.operator or "",
|
|
"total": total,
|
|
"done": done,
|
|
"remaining": max(0, total - done),
|
|
"success": success,
|
|
"failed": failed,
|
|
"progress": f"{success}/{total}" if total else "—",
|
|
"results": results,
|
|
"started_at": row.started_at.isoformat() if row.started_at else None,
|
|
"completed_at": row.completed_at.isoformat() if row.completed_at else None,
|
|
}
|