From 44b52d05cccd21cddfd84ffc915ca74ee226308b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 22 Jul 2026 10:04:07 +0200 Subject: [PATCH] fix: cinematic output type guard + legacy_only migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- LEARNINGS.md | 3 + .../070_seed_cinematic_output_type.py | 59 +++++++++++++++++++ .../071_fix_cinematic_output_type_rollout.py | 35 +++++++++++ backend/app/api/routers/output_types.py | 15 +++++ .../app/domains/rendering/dispatch_service.py | 24 ++++++++ 5 files changed, 136 insertions(+) create mode 100644 backend/alembic/versions/070_seed_cinematic_output_type.py create mode 100644 backend/alembic/versions/071_fix_cinematic_output_type_rollout.py diff --git a/LEARNINGS.md b/LEARNINGS.md index 56de2aa..acfe722 100644 --- a/LEARNINGS.md +++ b/LEARNINGS.md @@ -7,6 +7,9 @@ ## 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 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). diff --git a/backend/alembic/versions/070_seed_cinematic_output_type.py b/backend/alembic/versions/070_seed_cinematic_output_type.py new file mode 100644 index 0000000..e87e51b --- /dev/null +++ b/backend/alembic/versions/070_seed_cinematic_output_type.py @@ -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) + ) diff --git a/backend/alembic/versions/071_fix_cinematic_output_type_rollout.py b/backend/alembic/versions/071_fix_cinematic_output_type_rollout.py new file mode 100644 index 0000000..0c7abcd --- /dev/null +++ b/backend/alembic/versions/071_fix_cinematic_output_type_rollout.py @@ -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 diff --git a/backend/app/api/routers/output_types.py b/backend/app/api/routers/output_types.py index 7d744fa..1330cf0 100644 --- a/backend/app/api/routers/output_types.py +++ b/backend/app/api/routers/output_types.py @@ -271,6 +271,13 @@ async def create_output_type( if body.workflow_definition_id is None: 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) db.add(ot) await db.commit() @@ -387,6 +394,14 @@ async def update_output_type( if candidate_workflow_definition_id is None: 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(): setattr(ot, field_name, value) await db.commit() diff --git a/backend/app/domains/rendering/dispatch_service.py b/backend/app/domains/rendering/dispatch_service.py index 4821dc9..f717745 100644 --- a/backend/app/domains/rendering/dispatch_service.py +++ b/backend/app/domains/rendering/dispatch_service.py @@ -193,6 +193,30 @@ def dispatch_render_with_workflow(order_line_id: str) -> dict: ) 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") workflow_rollout_mode = _normalize_workflow_rollout_mode( getattr(output_type, "workflow_rollout_mode", None)