118 lines
3.5 KiB
Python
118 lines
3.5 KiB
Python
"""Background preparation and observable progress for batch report downloads."""
|
|
|
|
import threading
|
|
import time
|
|
|
|
from apps.admin_console.services import report_export
|
|
|
|
_lock = threading.Lock()
|
|
_states: dict[str, dict] = {}
|
|
|
|
|
|
def archive_path(batch_id: str):
|
|
return report_export.EXPORT_ROOT / "batches" / batch_id / "批次报告.zip"
|
|
|
|
|
|
def _snapshot(state: dict) -> dict:
|
|
elapsed = max(0, (state.get("finished_at") or time.monotonic()) - state["started_at"])
|
|
completed, total = state["completed"], state["total"]
|
|
eta = (
|
|
max(0, round(elapsed / completed * (total - completed)))
|
|
if completed and total > completed
|
|
else None
|
|
)
|
|
return {
|
|
"status": state["status"],
|
|
"stage": state["stage"],
|
|
"completed": completed,
|
|
"total": total,
|
|
"elapsed_seconds": round(elapsed),
|
|
"eta_seconds": eta,
|
|
"error": state.get("error"),
|
|
}
|
|
|
|
|
|
def status(batch_id: str, batch: dict) -> dict:
|
|
with _lock:
|
|
state = _states.get(batch_id)
|
|
if state:
|
|
return _snapshot(state)
|
|
if batch.get("report_ready") and archive_path(batch_id).is_file():
|
|
return {
|
|
"status": "ready",
|
|
"stage": "报告包已就绪",
|
|
"completed": 0,
|
|
"total": 0,
|
|
"elapsed_seconds": 0,
|
|
"eta_seconds": None,
|
|
"error": None,
|
|
}
|
|
return {
|
|
"status": "idle",
|
|
"stage": "尚未开始打包",
|
|
"completed": 0,
|
|
"total": 0,
|
|
"elapsed_seconds": 0,
|
|
"eta_seconds": None,
|
|
"error": None,
|
|
}
|
|
|
|
|
|
def prepare(batch_id: str, batch: dict) -> dict:
|
|
with _lock:
|
|
current = _states.get(batch_id)
|
|
if current and current["status"] == "preparing":
|
|
return _snapshot(current)
|
|
if batch.get("report_ready") and archive_path(batch_id).is_file():
|
|
_states.pop(batch_id, None)
|
|
return status_from_cache()
|
|
state = {
|
|
"status": "preparing",
|
|
"stage": "等待打包",
|
|
"completed": 0,
|
|
"total": len(report_export.store.jobs(batch_id)),
|
|
"started_at": time.monotonic(),
|
|
}
|
|
_states[batch_id] = state
|
|
|
|
def update(completed: int, total: int, stage: str) -> None:
|
|
with _lock:
|
|
state.update(completed=completed, total=total, stage=stage)
|
|
|
|
def run() -> None:
|
|
try:
|
|
with report_export.EXPORT_LOCK:
|
|
latest = report_export.store.get("batch", batch_id)
|
|
if not (latest and latest.get("report_ready") and archive_path(batch_id).is_file()):
|
|
report_export.export_batch(batch_id, progress=update)
|
|
with _lock:
|
|
state.update(status="ready", stage="报告包已就绪", finished_at=time.monotonic())
|
|
except Exception as exc:
|
|
with _lock:
|
|
state.update(
|
|
status="failed",
|
|
stage="打包失败",
|
|
error=str(exc)[:500],
|
|
finished_at=time.monotonic(),
|
|
)
|
|
|
|
threading.Thread(target=run, name=f"batch-archive-{batch_id[:8]}", daemon=True).start()
|
|
return status(batch_id, batch)
|
|
|
|
|
|
def status_from_cache() -> dict:
|
|
return {
|
|
"status": "ready",
|
|
"stage": "报告包已就绪",
|
|
"completed": 0,
|
|
"total": 0,
|
|
"elapsed_seconds": 0,
|
|
"eta_seconds": None,
|
|
"error": None,
|
|
}
|
|
|
|
|
|
def forget(batch_id: str) -> None:
|
|
with _lock:
|
|
_states.pop(batch_id, None)
|