feat: add duplicate-safe workflow shadow dispatch

This commit is contained in:
2026-04-07 11:35:32 +02:00
parent 26046fb2d6
commit f43f1e7420
11 changed files with 496 additions and 113 deletions
@@ -389,6 +389,7 @@ def prepare_order_line_render_context(
order_line_id: str,
*,
emit: EmitFn = None,
persist_state: bool = True,
) -> OrderLineRenderSetupResult:
"""Load and validate the order line, then prepare reusable render inputs."""
_emit(emit, order_line_id, "Loading order line from database")
@@ -421,7 +422,7 @@ def prepare_order_line_render_context(
if order and order.status in (OrderStatus.rejected, OrderStatus.completed):
_emit(emit, order_line_id, f"Order {order.status.value} — skipping render")
logger.info("OrderLine %s: order %s — skipping", order_line_id, order.status.value)
if line.render_status in ("pending", "processing"):
if persist_state and line.render_status in ("pending", "processing"):
session.execute(
sql_update(OrderLine)
.where(OrderLine.id == line.id)
@@ -438,12 +439,13 @@ def prepare_order_line_render_context(
if line.product is None or line.product.cad_file_id is None or line.product.cad_file is None:
_emit(emit, order_line_id, "Product has no CAD file — marking as failed", "error")
logger.warning("OrderLine %s: product has no CAD file", order_line_id)
session.execute(
sql_update(OrderLine)
.where(OrderLine.id == line.id)
.values(render_status="failed")
)
session.commit()
if persist_state:
session.execute(
sql_update(OrderLine)
.where(OrderLine.id == line.id)
.values(render_status="failed")
)
session.commit()
return OrderLineRenderSetupResult(
status="failed",
order_line=line,
@@ -451,17 +453,18 @@ def prepare_order_line_render_context(
reason="missing_cad_file",
)
render_start = datetime.utcnow()
session.execute(
sql_update(OrderLine)
.where(OrderLine.id == line.id)
.values(
render_status="processing",
render_backend_used="celery",
render_started_at=render_start,
render_start = datetime.utcnow() if persist_state else None
if persist_state:
session.execute(
sql_update(OrderLine)
.where(OrderLine.id == line.id)
.values(
render_status="processing",
render_backend_used="celery",
render_started_at=render_start,
)
)
)
session.commit()
session.commit()
cad_file = line.product.cad_file
materials_source = line.product.cad_part_materials or []
@@ -665,6 +668,7 @@ def auto_populate_materials_for_cad(
cad_file_id: str,
*,
enqueue_thumbnail: QueueThumbnailFn = None,
persist_updates: bool = True,
) -> AutoPopulateMaterialsResult:
"""Auto-fill empty CAD material mappings from Excel component data.
@@ -708,13 +712,14 @@ def auto_populate_materials_for_cad(
continue
new_materials = build_materials_from_excel(cad_parts, excel_components)
session.execute(
sql_update(Product)
.where(Product.id == product.id)
.values(cad_part_materials=new_materials)
)
session.flush()
updated_product_ids.append(str(product.id))
if persist_updates:
session.execute(
sql_update(Product)
.where(Product.id == product.id)
.values(cad_part_materials=new_materials)
)
session.flush()
try:
final_part_colors = build_part_colors(cad_parts, new_materials)
@@ -728,10 +733,11 @@ def auto_populate_materials_for_cad(
len(excel_components),
)
session.commit()
if persist_updates:
session.commit()
queued_thumbnail_regeneration = False
if final_part_colors is not None:
if persist_updates and final_part_colors is not None:
if enqueue_thumbnail is None:
from app.domains.pipeline.tasks.render_thumbnail import regenerate_thumbnail