Панель: журнал заданий, pull --progress plain, PTY/EIO, старт/стоп в фоне, очистка jobs
- Фоновые stop/start с job_id и poll; отмена с kill; docker pull plain + снятие ANSI - Лимиты журнала API/буфера; список jobs без progress_log; DELETE /jobs - UI: опрос чаще, подсказка при пустом логе, кнопка очистки завершённых
This commit is contained in:
+72
-7
@@ -13,6 +13,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import threading
|
||||
import uuid
|
||||
from collections import deque
|
||||
@@ -33,10 +35,14 @@ _cancel_events: dict[str, threading.Event] = {}
|
||||
_progress: dict[str, tuple[str, int]] = {}
|
||||
# Хвост логов для активных заданий (kind create и т.д.); после завершения копируется в JobRecord.log_lines
|
||||
_job_log_deques: dict[str, deque[str]] = {}
|
||||
# Активный дочерний процесс (pull / kind create) — для принудительной отмены
|
||||
_process_lock = threading.Lock()
|
||||
_active_subprocess_by_job: dict[str, subprocess.Popen] = {}
|
||||
|
||||
|
||||
def _max_job_log_lines() -> int:
|
||||
raw = (os.environ.get("KIND_K8S_JOB_LOG_MAX_LINES") or "500").strip()
|
||||
# Буфер в памяти: старые строки вытесняются; по умолчанию 2500 — виден почти весь типичный лог.
|
||||
raw = (os.environ.get("KIND_K8S_JOB_LOG_MAX_LINES") or "2500").strip()
|
||||
try:
|
||||
return max(50, min(int(raw), 5000))
|
||||
except ValueError:
|
||||
@@ -82,6 +88,8 @@ def begin_job_tracking(job_id: str) -> None:
|
||||
|
||||
def end_job_tracking(job_id: str) -> None:
|
||||
"""Очистить служебные структуры после завершения задания."""
|
||||
with _process_lock:
|
||||
_active_subprocess_by_job.pop(job_id, None)
|
||||
with _thread_lock:
|
||||
_cancel_events.pop(job_id, None)
|
||||
_progress.pop(job_id, None)
|
||||
@@ -102,19 +110,52 @@ def get_progress_sync(job_id: str) -> tuple[str, int] | None:
|
||||
return _progress.get(job_id)
|
||||
|
||||
|
||||
def register_subprocess_sync(job_id: str, proc: subprocess.Popen) -> None:
|
||||
"""Привязать текущий дочерний процесс к заданию (pull, kind create)."""
|
||||
with _process_lock:
|
||||
_active_subprocess_by_job[job_id] = proc
|
||||
|
||||
|
||||
def unregister_subprocess_sync(job_id: str) -> None:
|
||||
"""Снять регистрацию процесса (после завершения потока чтения)."""
|
||||
with _process_lock:
|
||||
_active_subprocess_by_job.pop(job_id, None)
|
||||
|
||||
|
||||
def kill_subprocess_for_job_sync(job_id: str) -> None:
|
||||
"""Принудительно завершить дочерний процесс задания (SIGKILL / группа процессов)."""
|
||||
with _process_lock:
|
||||
proc = _active_subprocess_by_job.get(job_id)
|
||||
if proc is None:
|
||||
return
|
||||
if proc.poll() is not None:
|
||||
return
|
||||
pid = proc.pid
|
||||
try:
|
||||
try:
|
||||
os.killpg(os.getpgid(pid), signal.SIGKILL)
|
||||
logger.warning("Отмена: SIGKILL группы процесса PID %s (задание %s)", pid, job_id)
|
||||
except (AttributeError, OSError, ProcessLookupError):
|
||||
proc.kill()
|
||||
logger.warning("Отмена: kill PID %s (задание %s)", pid, job_id)
|
||||
except OSError as e:
|
||||
logger.warning("Не удалось убить процесс задания %s: %s", job_id, e)
|
||||
|
||||
|
||||
def request_cancel_sync(job_id: str) -> bool:
|
||||
"""Запросить отмену. Вернуть False, если job_id не отслеживается."""
|
||||
"""Запросить отмену и прервать активный подпроцесс (если есть)."""
|
||||
with _thread_lock:
|
||||
ev = _cancel_events.get(job_id)
|
||||
if ev is None:
|
||||
return False
|
||||
ev.set()
|
||||
logger.info("Запрошена отмена задания %s", job_id)
|
||||
return True
|
||||
kill_subprocess_for_job_sync(job_id)
|
||||
return True
|
||||
|
||||
|
||||
def is_cancelled_sync(job_id: str) -> bool:
|
||||
"""Проверка из worker-thread между этапами создания кластера."""
|
||||
"""Проверка из worker-thread между этапами и перед каждой следующей командой."""
|
||||
with _thread_lock:
|
||||
ev = _cancel_events.get(job_id)
|
||||
return bool(ev and ev.is_set())
|
||||
@@ -164,12 +205,18 @@ class JobStore:
|
||||
logger.info("Создано задание %s kind=%s cluster=%s", jid, kind, cluster_name)
|
||||
return rec
|
||||
|
||||
async def set_running(self, job_id: str) -> None:
|
||||
async def set_running(
|
||||
self,
|
||||
job_id: str,
|
||||
*,
|
||||
stage: str = "Выполняется…",
|
||||
percent: int = 5,
|
||||
) -> None:
|
||||
async with self._lock:
|
||||
if job_id in self._jobs:
|
||||
self._jobs[job_id].status = "running"
|
||||
self._jobs[job_id].message = None
|
||||
set_progress_sync(job_id, "Запуск создания кластера…", 5)
|
||||
set_progress_sync(job_id, stage, percent)
|
||||
|
||||
async def set_success(self, job_id: str, *, result: dict[str, Any] | None = None, message: str | None = None) -> None:
|
||||
logs = take_logs_finalize_sync(job_id)
|
||||
@@ -190,7 +237,7 @@ class JobStore:
|
||||
self._jobs[job_id].log_lines = logs
|
||||
logger.warning("Задание %s завершилось ошибкой: %s", job_id, message)
|
||||
|
||||
async def set_cancelled(self, job_id: str, message: str = "Создание отменено пользователем") -> None:
|
||||
async def set_cancelled(self, job_id: str, message: str = "Операция отменена") -> None:
|
||||
logs = take_logs_finalize_sync(job_id)
|
||||
async with self._lock:
|
||||
if job_id in self._jobs:
|
||||
@@ -213,6 +260,24 @@ class JobStore:
|
||||
items.sort(key=lambda r: r.created_at_utc, reverse=True)
|
||||
return items[: max(1, limit)]
|
||||
|
||||
async def purge_completed(self) -> int:
|
||||
"""
|
||||
Удалить из памяти только завершённые задания (success / failed / cancelled).
|
||||
|
||||
Активные (queued, running) не трогаем. Служебные структуры отслеживания подчищаются.
|
||||
"""
|
||||
async with self._lock:
|
||||
to_remove = [
|
||||
jid
|
||||
for jid, rec in self._jobs.items()
|
||||
if rec.status in ("success", "failed", "cancelled")
|
||||
]
|
||||
for jid in to_remove:
|
||||
del self._jobs[jid]
|
||||
for jid in to_remove:
|
||||
end_job_tracking(jid)
|
||||
return len(to_remove)
|
||||
|
||||
|
||||
# Синглтон на процесс uvicorn
|
||||
job_store = JobStore()
|
||||
|
||||
Reference in New Issue
Block a user