fix: cinematic output type guard + legacy_only migration

- Migration 070: seeds 'Cinematic Highlight' output type (legacy_only, no
  workflow link) for fresh installs
- Migration 071: patches existing cinematic output types — clears
  workflow_definition_id and forces legacy_only rollout mode. The row
  existed since 2026-03 with shadow mode + a linked workflow def;
  no BLENDER_CINEMATIC graph node exists so shadow execution would fail.
- API guard in output_types POST + PATCH: cinematic output types cannot
  receive a workflow_definition_id (HTTP 400)
- Defense-in-depth in dispatch_service: early legacy exit if
  render_settings.cinematic is true, regardless of rollout mode
- docs: learning erfasst — cinematic rollout mode footgun

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 10:04:07 +02:00
co-authored by Claude Sonnet 4.6
parent 971074fedd
commit 44b52d05cc
5 changed files with 136 additions and 0 deletions
+3
View File
@@ -7,6 +7,9 @@
## Learnings ## Learnings
### 2026-07-22 | Architecture | Cinematic Output Type existierte bereits mit falschem workflow_rollout_mode
Der "Cinematic Highlight" Output-Type war seit März 2026 in der DB mit `workflow_rollout_mode = shadow` und einer `workflow_definition_id` gesetzt. Da es keinen `BLENDER_CINEMATIC`-Node im Workflow-Graph gibt, hätte jeder Cinematic-Render eine Shadow-Graph-Execution ausgelöst, die still scheitert. Fix via Migration 071: alle cinematic Output-Types auf `legacy_only` + `workflow_definition_id = NULL` patchen. Zusätzlich API-Guard in POST/PATCH `output_types.py` eingebaut: cinematic + workflow_definition_id → 400 Error. Defense-in-depth im `dispatch_service.py`: frühzeitiger Legacy-Exit wenn `render_settings.cinematic = true`.
### 2026-03-15 | Architecture | Per-order-line render overrides via JSONB ### 2026-03-15 | Architecture | Per-order-line render overrides via JSONB
Render overrides (JSONB) on OrderLine allow overriding any output type render setting (format, resolution, samples, engine, etc.) at order time without duplicating output types. Applied AFTER output type render_settings AND after transparent_bg/cycles_device_val assignment, so they take final priority. Also affects dispatch queue routing (width/height overrides change light vs heavy queue routing). Render overrides (JSONB) on OrderLine allow overriding any output type render setting (format, resolution, samples, engine, etc.) at order time without duplicating output types. Applied AFTER output type render_settings AND after transparent_bg/cycles_device_val assignment, so they take final priority. Also affects dispatch queue routing (width/height overrides change light vs heavy queue routing).
@@ -0,0 +1,59 @@
"""Seed 'Cinematic Highlight' output type.
Revision ID: 070
Revises: 069
"""
from alembic import op
import sqlalchemy as sa
revision = "070"
down_revision = "069"
branch_labels = None
depends_on = None
_NAME = "Cinematic Highlight"
def upgrade() -> None:
op.execute(
sa.text(
f"""
INSERT INTO output_types (
id, name, description, renderer, render_settings,
output_format, sort_order, compatible_categories, render_backend,
is_animation, transparent_bg, workflow_family, artifact_kind,
invocation_overrides, cycles_device, pricing_tier_id, is_active,
tenant_id, material_override, workflow_definition_id,
workflow_rollout_mode, created_at, updated_at
) VALUES (
gen_random_uuid(),
'{_NAME}',
'Cinematic highlight animation: 4-segment camera orbit, depth-of-field, '
'250 frames @ 25 fps (10 s), 1920x1080 MP4. '
'Runs legacy-only — no workflow graph node exists for cinematic.',
'blender',
'{{"cinematic": true, "samples": 128, "width": 1920, "height": 1080}}'::jsonb,
'mp4',
50,
'[]'::jsonb,
'celery',
true,
false,
'order_line',
'turntable_video',
'{{}}'::jsonb,
NULL, NULL, true, NULL, NULL, NULL,
'legacy_only',
now(), now()
)
ON CONFLICT (name) DO NOTHING
"""
)
)
def downgrade() -> None:
op.execute(
sa.text("DELETE FROM output_types WHERE name = :name").bindparams(name=_NAME)
)
@@ -0,0 +1,35 @@
"""Fix cinematic output types: force legacy_only rollout, clear workflow link.
No BLENDER_CINEMATIC node exists in the workflow graph system. Any cinematic
output type with a workflow_definition_id set would cause the shadow/graph
execution path to attempt a graph run and fail silently.
Revision ID: 071
Revises: 070
"""
from alembic import op
import sqlalchemy as sa
revision = "071"
down_revision = "070"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
sa.text(
"""
UPDATE output_types
SET
workflow_rollout_mode = 'legacy_only',
workflow_definition_id = NULL
WHERE (render_settings->>'cinematic')::boolean IS TRUE
"""
)
)
def downgrade() -> None:
pass
+15
View File
@@ -271,6 +271,13 @@ async def create_output_type(
if body.workflow_definition_id is None: if body.workflow_definition_id is None:
data["workflow_rollout_mode"] = "legacy_only" data["workflow_rollout_mode"] = "legacy_only"
if data.get("render_settings", {}).get("cinematic") and body.workflow_definition_id is not None:
raise HTTPException(
400,
detail="Cinematic output types cannot be linked to a workflow definition. "
"No BLENDER_CINEMATIC node exists in the workflow graph; cinematic renders use the legacy path only.",
)
ot = OutputType(**data) ot = OutputType(**data)
db.add(ot) db.add(ot)
await db.commit() await db.commit()
@@ -387,6 +394,14 @@ async def update_output_type(
if candidate_workflow_definition_id is None: if candidate_workflow_definition_id is None:
data["workflow_rollout_mode"] = "legacy_only" data["workflow_rollout_mode"] = "legacy_only"
candidate_render_settings = data.get("render_settings", ot.render_settings) or {}
if candidate_render_settings.get("cinematic") and candidate_workflow_definition_id is not None:
raise HTTPException(
400,
detail="Cinematic output types cannot be linked to a workflow definition. "
"No BLENDER_CINEMATIC node exists in the workflow graph; cinematic renders use the legacy path only.",
)
for field_name, value in data.items(): for field_name, value in data.items():
setattr(ot, field_name, value) setattr(ot, field_name, value)
await db.commit() await db.commit()
@@ -193,6 +193,30 @@ def dispatch_render_with_workflow(order_line_id: str) -> dict:
) )
return legacy_result return legacy_result
# Cinematic output types have no BLENDER_CINEMATIC graph node — force legacy regardless
# of what workflow_rollout_mode or workflow_definition_id say.
if output_type and isinstance(output_type.render_settings, dict) and output_type.render_settings.get("cinematic"):
logger.warning(
"order_line %s: output_type %s is cinematic but has a workflow_definition_id set; "
"forcing legacy dispatch (no BLENDER_CINEMATIC node exists in the workflow graph)",
order_line_id,
output_type.id,
)
legacy_result = _legacy_dispatch(order_line_id)
legacy_result.update(
_build_rollout_signal(
gate_status="cinematic_legacy_only",
ready=False,
reasons=[
"Cinematic output types always use the legacy dispatch path.",
"Remove the workflow_definition_id link to silence this warning.",
],
workflow_def_id=wf_def.id,
output_type_id=output_type.id,
)
)
return legacy_result
configured_execution_mode = get_workflow_execution_mode(canonical_config, default="legacy") configured_execution_mode = get_workflow_execution_mode(canonical_config, default="legacy")
workflow_rollout_mode = _normalize_workflow_rollout_mode( workflow_rollout_mode = _normalize_workflow_rollout_mode(
getattr(output_type, "workflow_rollout_mode", None) getattr(output_type, "workflow_rollout_mode", None)