feat(data-process): 完善后台生成与失败重试
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user