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:
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user