Files
vfxreview/lib/render-pipeline/transitions.ts
T
twotalesanimation ae58dc0366 feat(pipeline): render queue, worker service and automated preview generation
One "Queue Export" click now renders the EXR sequence, then rebuilds the shot
headlessly with the studio slate/overlay template to produce the delivery MOV
and review MP4. Implements RenderPipeline2 phases 1-2 plus the preview stage.

Server:
- New models Export, RenderJob, ExportEvent, Machine, WorkerHeartbeat, plus
  Project.deliveryConfig and per-submission slate fields (Export.vfxScope,
  Export.submissionNote, inherited from the shot's previous export).
  Both migrations are purely additive; no existing column is touched.
- lib/render-pipeline: server-enforced state machine, transactional version
  increment with supersede, atomic FOR UPDATE SKIP LOCKED claim gated by
  machine availability windows, and a lease reaper run from instrumentation.ts.
- /api/ext/* endpoints for the panel and workers; session-auth mirrors under
  /api/render and /api/machines for the web UI.
- Pipeline pages: render queue, export detail, machine monitoring, plus an
  Exports tab on shot detail.

RenderWorker (.NET 8 Windows service, new):
- Registration, heartbeat as cancel channel, claim loop, aerender runner with
  progress parsing and stall watchdog, crash recovery and disk-spooled
  reporting that survives server downtime.
- Preview stage: headless AE assembles the preview comp into a throwaway AEP
  with both output modules queued, then a single aerender pass renders them.
  Preview jobs are not claimed while an interactive AE session is open, so an
  artist's project is never taken over.

AE panel: Queue Export with live status polling, urgent flag, retry, and the
VFX Scope / Submission Note fields. Every existing panel action is unchanged.

Preview chaining ships disabled behind SystemConfig preview.enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 14:34:34 +02:00

78 lines
2.6 KiB
TypeScript

import { ExportStatus, Prisma } from "@prisma/client";
import { PipelineError } from "./errors";
/**
* Export lifecycle state machine (RenderPipeline2 §4).
* The server is the sole authority on state; workers *request* transitions.
*
* NOTE (Phase 2 interim): RENDERING → READY_FOR_QC is temporarily legal.
* Phase 3 inserts VALIDATING between them (§17.3) — remove READY_FOR_QC from
* RENDERING's list when the validation engine ships.
*/
export const EXPORT_TRANSITIONS: Record<ExportStatus, ExportStatus[]> = {
QUEUED: ["RENDERING", "CANCELLED", "SUPERSEDED", "READY_FOR_QC"], // READY_FOR_QC: manual mark-done only
RENDERING: [
"VALIDATING",
"GENERATING_PREVIEW", // interim: preview stage runs before validation exists
"RENDER_FAILED",
"QUEUED", // lease expiry / retryable fail with attempts remaining (T5)
"CANCELLED",
"READY_FOR_QC", // preview disabled / manual mark-done
],
RENDER_FAILED: ["QUEUED", "SUPERSEDED"],
VALIDATING: ["VALIDATION_FAILED", "GENERATING_PREVIEW"],
VALIDATION_FAILED: ["QUEUED", "SUPERSEDED"],
GENERATING_PREVIEW: ["PREVIEW_FAILED", "READY_FOR_QC"],
PREVIEW_FAILED: ["GENERATING_PREVIEW", "SUPERSEDED"],
READY_FOR_QC: ["QC_FAILED", "READY_FOR_DELIVERY", "SUPERSEDED"],
QC_FAILED: ["SUPERSEDED"],
READY_FOR_DELIVERY: ["PACKAGED", "SUPERSEDED"],
PACKAGED: ["DELIVERED"],
DELIVERED: ["ARCHIVED"],
SUPERSEDED: [],
ARCHIVED: [],
CANCELLED: [],
};
export type TransitionActor = {
type: "WORKER" | "USER" | "SYSTEM";
id?: string | null;
note?: string | null;
};
export function isTransitionAllowed(from: ExportStatus, to: ExportStatus): boolean {
return EXPORT_TRANSITIONS[from]?.includes(to) ?? false;
}
/**
* Applies a state transition inside an existing transaction: validates
* legality, stamps statusChangedAt, appends the ExportEvent audit row.
* Throws PipelineError(409) on an illegal transition (§4.2 rules).
*/
export async function applyTransition(
tx: Prisma.TransactionClient,
exportRow: { id: string; status: ExportStatus },
to: ExportStatus,
actor: TransitionActor,
extraData: Prisma.ExportUpdateInput = {}
) {
if (!isTransitionAllowed(exportRow.status, to)) {
throw new PipelineError(409, `Invalid transition ${exportRow.status}${to}`);
}
const updated = await tx.export.update({
where: { id: exportRow.id },
data: { status: to, statusChangedAt: new Date(), ...extraData },
});
await tx.exportEvent.create({
data: {
exportId: exportRow.id,
fromStatus: exportRow.status,
toStatus: to,
actorType: actor.type,
actorId: actor.id ?? null,
note: actor.note ?? null,
},
});
return updated;
}