feat(data-process): 完善后台生成与失败重试

This commit is contained in:
caoxiaozhu
2026-07-28 10:56:05 +08:00
parent 8a6a6574bb
commit 01d2e6c76a
178 changed files with 4472 additions and 595 deletions

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import inspect
import json
from collections.abc import Iterator
from contextlib import contextmanager
@@ -20,6 +21,14 @@ from app.modules.data_process.store import (
)
def test_preview_replace_sql_never_uses_untyped_null_placeholders() -> None:
source = inspect.getsource(DataProcessStore.replace_preview_items)
assert "%s IS NULL" not in source
assert "is_direct_build = preview_run_id is None" in source
assert "workflow_step=CASE WHEN %s THEN 'preview'" in source
class _Result:
def __init__(self, *, row: dict[str, Any] | None = None, rows: list[dict[str, Any]] | None = None):
self.row = row
@@ -146,6 +155,7 @@ class _PublishStore(DataProcessStore):
return {
"id": task_id,
"status": "completed",
"results_confirmed": True,
"description": "",
"config": self._task_config,
"output_dataset_id": train_dataset and train_dataset["id"],
@@ -454,12 +464,14 @@ class _LegacyRecoveryConnection:
record["preview_item_id"] = params[1]
return _Result()
if normalized.startswith("UPDATE data_process_tasks SET status='completed'"):
assert "workflow_step='results'" in normalized
self.task.update(
{
"status": "completed",
"progress": 100,
"output_dataset_id": params[0],
"output_count": params[1],
"workflow_step": "results",
}
)
return _Result()
@@ -496,6 +508,7 @@ class _StartGenerationConnection:
config=config,
output_dataset_id="dataset_train" if published_prepared else None,
output_count=28,
results_confirmed=published_prepared,
),
"generation_run_id": None,
}
@@ -512,6 +525,7 @@ class _StartGenerationConnection:
assert normalized.startswith("UPDATE data_process_tasks SET config=%s, status='running'")
assert "output_dataset_id=NULL" in normalized
assert "output_count=0" in normalized
assert "results_confirmed=FALSE" in normalized
self.task.update(
{
"config": params[0],
@@ -526,6 +540,7 @@ class _StartGenerationConnection:
"duplicate_count": 0,
"error_count": 0,
"generation_run_id": params[2],
"results_confirmed": False,
"updated_at": params[3],
}
)
@@ -713,6 +728,7 @@ def test_start_generation_clears_previous_output_count() -> None:
assert task["status"] == "running"
assert task["output_count"] == 0
assert task["results_confirmed"] is False
assert conn.results == []
@@ -737,6 +753,7 @@ def test_prepared_published_task_survives_generation_preflight_failure() -> None
assert conn.task["status"] == "completed"
assert conn.task["output_dataset_id"] == "dataset_train"
assert conn.task["output_count"] == 28
assert conn.task["results_confirmed"] is True
assert "_regeneration_prepared" in conn.task["config"]
assert conn.results == [{"id": "old-result"}]
@@ -752,6 +769,7 @@ def test_legacy_aborted_regeneration_recovers_results_and_published_state() -> N
assert conn.task["progress"] == 100
assert conn.task["output_dataset_id"] == "dataset_train"
assert conn.task["output_count"] == 2
assert conn.task["workflow_step"] == "results"
assert conn.task["started_at"] is None
assert conn.task["completed_at"] is None
assert [item["id"] for item in conn.results] == ["result_train", "result_test"]
@@ -1185,3 +1203,96 @@ def test_source_storage_descriptor_rejects_unowned_or_unsupported_references(
"dpt_task",
"dpsf_source",
)
class _LifecycleConnection:
def __init__(self) -> None:
self.task: dict[str, Any] = {
"id": "task-lifecycle",
"status": "running",
"generation_run_id": "generation-active",
"workflow_step": "generate",
"preview_status": "running",
"preview_progress": Decimal("40.00"),
"preview_run_id": "preview-active",
"preview_failure_reason": None,
"preview_total_files": 5,
"preview_completed_files": 2,
"deleted_at": None,
"deleted_by": None,
}
self.last_update_sql = ""
def execute(self, sql: str, params: Any = None) -> _Result:
normalized = " ".join(sql.split())
if params is not None:
assert normalized.count("%s") == len(params)
if normalized.startswith("SELECT * FROM data_process_tasks"):
row = None if self.task["deleted_at"] is not None else dict(self.task)
return _Result(row=row)
if normalized.startswith("UPDATE data_process_tasks SET workflow_step="):
self.last_update_sql = normalized
workflow_step, updated_at, task_id = params
assert task_id == self.task["id"]
self.task.update(workflow_step=workflow_step, updated_at=updated_at)
return _Result(row=dict(self.task))
if normalized.startswith("UPDATE data_process_tasks SET status=CASE"):
self.last_update_sql = normalized
deleted_at, deleted_by, updated_at, task_id = params
assert task_id == self.task["id"]
self.task.update(
status="stopped",
generation_run_id=None,
preview_status="cancelled",
preview_run_id=None,
deleted_at=deleted_at,
deleted_by=deleted_by,
updated_at=updated_at,
)
return _Result()
if normalized.startswith("SELECT status, generation_run_id"):
row = None if self.task["deleted_at"] is not None else dict(self.task)
return _Result(row=row)
if normalized.startswith("SELECT preview_status, preview_run_id"):
row = None if self.task["deleted_at"] is not None else dict(self.task)
return _Result(row=row)
raise AssertionError(f"unexpected SQL: {normalized}")
class _LifecycleStore(DataProcessStore):
def __init__(self, conn: _LifecycleConnection) -> None:
self._conn = conn
@contextmanager
def connect(self) -> Iterator[_LifecycleConnection]:
yield self._conn
def test_workflow_step_update_does_not_invalidate_active_runs() -> None:
conn = _LifecycleConnection()
task = _LifecycleStore(conn).update_workflow_step("task-lifecycle", "results")
assert task["workflow_step"] == "results"
assert task["status"] == "running"
assert task["generation_run_id"] == "generation-active"
assert task["preview_status"] == "running"
assert task["preview_run_id"] == "preview-active"
assert "generation_run_id" not in conn.last_update_sql
assert "preview_run_id" not in conn.last_update_sql
def test_delete_atomically_invalidates_generation_and_preview_runs() -> None:
conn = _LifecycleConnection()
store = _LifecycleStore(conn)
store.delete_task("task-lifecycle", deleted_by="user-1")
assert conn.task["status"] == "stopped"
assert conn.task["generation_run_id"] is None
assert conn.task["preview_status"] == "cancelled"
assert conn.task["preview_run_id"] is None
assert conn.task["deleted_by"] == "user-1"
assert conn.task["deleted_at"] is not None
assert store.generation_is_running("task-lifecycle", "generation-active") is False
assert store.preview_is_running("task-lifecycle", "preview-active") is False