feat: add graph workflow fallback and retry metadata

This commit is contained in:
2026-04-07 10:56:45 +02:00
parent c17b7d2e8f
commit f9d4da52b9
9 changed files with 473 additions and 39 deletions
@@ -16,8 +16,9 @@ from app.domains.orders.models import Order, OrderLine, OrderStatus
from app.domains.products.models import CadFile, Product
from app.domains.rendering.models import OutputType, RenderTemplate, WorkflowRun
from app.domains.rendering.workflow_executor import prepare_workflow_context
from app.domains.rendering.workflow_graph_runtime import execute_graph_workflow
from app.domains.rendering.workflow_graph_runtime import WorkflowGraphRuntimeError, execute_graph_workflow
from app.domains.rendering.workflow_run_service import create_workflow_run
from app.domains.rendering.workflow_runtime_services import OrderLineRenderSetupResult
import app.models # noqa: F401
@@ -239,3 +240,116 @@ def test_execute_graph_workflow_persists_bridge_outputs_and_queues_render_task(
{"InnerRing": "Steel", "OuterRing": "Rubber"},
)
]
def test_execute_graph_workflow_retries_bridge_node_and_persists_attempt_metadata(
sync_session,
monkeypatch,
):
attempts = {"count": 0}
def _flaky_prepare(_session, _context_id):
attempts["count"] += 1
if attempts["count"] == 1:
raise RuntimeError("temporary setup failure")
return OrderLineRenderSetupResult(status="skip", reason="line_cancelled")
monkeypatch.setattr(
"app.domains.rendering.workflow_graph_runtime.prepare_order_line_render_context",
_flaky_prepare,
)
workflow_context = prepare_workflow_context(
{
"version": 1,
"nodes": [
{
"id": "setup",
"step": "order_line_setup",
"params": {"retry_policy": {"max_attempts": 2}},
},
],
"edges": [],
},
context_id=str(uuid.uuid4()),
execution_mode="graph",
)
run = create_workflow_run(
sync_session,
workflow_def_id=None,
order_line_id=None,
workflow_context=workflow_context,
)
dispatch_result = execute_graph_workflow(sync_session, workflow_context)
sync_session.commit()
refreshed_run = sync_session.execute(
select(WorkflowRun)
.where(WorkflowRun.id == run.id)
.options(selectinload(WorkflowRun.node_results))
).scalar_one()
setup_result = next(node for node in refreshed_run.node_results if node.node_name == "setup")
assert dispatch_result.task_ids == []
assert refreshed_run.status == "completed"
assert setup_result.status == "skipped"
assert setup_result.output["attempt_count"] == 2
assert setup_result.output["max_attempts"] == 2
assert setup_result.output["retry_state"] == "recovered"
assert setup_result.output["last_error"] == "temporary setup failure"
assert setup_result.output["retry_policy"]["max_attempts"] == 2
def test_execute_graph_workflow_marks_failed_node_with_retry_exhausted_metadata(
sync_session,
monkeypatch,
):
monkeypatch.setattr(
"app.domains.rendering.workflow_graph_runtime.prepare_order_line_render_context",
lambda _session, _context_id: (_ for _ in ()).throw(RuntimeError("permanent setup failure")),
)
workflow_context = prepare_workflow_context(
{
"version": 1,
"nodes": [
{
"id": "setup",
"step": "order_line_setup",
"params": {
"retry_policy": {"max_attempts": 2},
"failure_policy": {"fallback_to_legacy": True},
},
},
],
"edges": [],
},
context_id=str(uuid.uuid4()),
execution_mode="graph",
)
run = create_workflow_run(
sync_session,
workflow_def_id=None,
order_line_id=None,
workflow_context=workflow_context,
)
with pytest.raises(WorkflowGraphRuntimeError, match="permanent setup failure"):
execute_graph_workflow(sync_session, workflow_context)
sync_session.commit()
refreshed_run = sync_session.execute(
select(WorkflowRun)
.where(WorkflowRun.id == run.id)
.options(selectinload(WorkflowRun.node_results))
).scalar_one()
setup_result = next(node for node in refreshed_run.node_results if node.node_name == "setup")
assert setup_result.status == "failed"
assert setup_result.output["attempt_count"] == 2
assert setup_result.output["max_attempts"] == 2
assert setup_result.output["retry_exhausted"] is True
assert setup_result.output["last_error"] == "permanent setup failure"
assert setup_result.output["failure_policy"]["fallback_to_legacy"] is True