feat(dashboard): persist task state to disk for restart recovery
- Add TASK_STATUS_DIR for task state JSON files - Lifespan startup: restore task states from disk - Task completion/failure: write state to disk - Task cancellation: delete persisted state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
30db76b9b1
commit
4b007c16ae
|
|
@ -23,6 +23,8 @@ from pydantic import BaseModel
|
||||||
REPO_ROOT = Path(__file__).parent.parent.parent
|
REPO_ROOT = Path(__file__).parent.parent.parent
|
||||||
# Use the currently running Python interpreter
|
# Use the currently running Python interpreter
|
||||||
ANALYSIS_PYTHON = Path(sys.executable)
|
ANALYSIS_PYTHON = Path(sys.executable)
|
||||||
|
# Task state persistence directory
|
||||||
|
TASK_STATUS_DIR = Path(__file__).parent / "data" / "task_status"
|
||||||
|
|
||||||
|
|
||||||
# ============== Lifespan ==============
|
# ============== Lifespan ==============
|
||||||
|
|
@ -33,6 +35,16 @@ async def lifespan(app: FastAPI):
|
||||||
app.state.active_connections: dict[str, list[WebSocket]] = {}
|
app.state.active_connections: dict[str, list[WebSocket]] = {}
|
||||||
app.state.task_results: dict[str, dict] = {}
|
app.state.task_results: dict[str, dict] = {}
|
||||||
app.state.analysis_tasks: dict[str, asyncio.Task] = {}
|
app.state.analysis_tasks: dict[str, asyncio.Task] = {}
|
||||||
|
|
||||||
|
# Restore persisted task states from disk
|
||||||
|
TASK_STATUS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
for f in TASK_STATUS_DIR.glob("*.json"):
|
||||||
|
try:
|
||||||
|
data = json.loads(f.read_text())
|
||||||
|
app.state.task_results[data["task_id"]] = data
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -86,7 +98,21 @@ def _load_from_cache(mode: str) -> Optional[dict]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _save_to_cache(mode: str, data: dict):
|
def _save_task_status(task_id: str, data: dict):
|
||||||
|
"""Persist task state to disk"""
|
||||||
|
try:
|
||||||
|
TASK_STATUS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
(TASK_STATUS_DIR / f"{task_id}.json").write_text(json.dumps(data, ensure_ascii=False))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_task_status(task_id: str):
|
||||||
|
"""Remove persisted task state from disk"""
|
||||||
|
try:
|
||||||
|
(TASK_STATUS_DIR / f"{task_id}.json").unlink(missing_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
cache_path = _get_cache_path(mode)
|
cache_path = _get_cache_path(mode)
|
||||||
|
|
@ -346,6 +372,8 @@ async def start_analysis(request: AnalysisRequest):
|
||||||
app.state.task_results[task_id]["status"] = "failed"
|
app.state.task_results[task_id]["status"] = "failed"
|
||||||
app.state.task_results[task_id]["error"] = error_msg
|
app.state.task_results[task_id]["error"] = error_msg
|
||||||
|
|
||||||
|
_save_task_status(task_id, app.state.task_results[task_id])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
cancel_event.set()
|
cancel_event.set()
|
||||||
app.state.task_results[task_id]["status"] = "failed"
|
app.state.task_results[task_id]["status"] = "failed"
|
||||||
|
|
@ -355,6 +383,8 @@ async def start_analysis(request: AnalysisRequest):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
_save_task_status(task_id, app.state.task_results[task_id])
|
||||||
|
|
||||||
await broadcast_progress(task_id, app.state.task_results[task_id])
|
await broadcast_progress(task_id, app.state.task_results[task_id])
|
||||||
|
|
||||||
task = asyncio.create_task(run_analysis())
|
task = asyncio.create_task(run_analysis())
|
||||||
|
|
@ -425,6 +455,9 @@ async def cancel_task(task_id: str):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Remove persisted task state
|
||||||
|
_delete_task_status(task_id)
|
||||||
|
|
||||||
return {"task_id": task_id, "status": "cancelled"}
|
return {"task_id": task_id, "status": "cancelled"}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue