diff --git a/lib/render-pipeline/preview.ts b/lib/render-pipeline/preview.ts index d3ce163..ebfa2b0 100644 --- a/lib/render-pipeline/preview.ts +++ b/lib/render-pipeline/preview.ts @@ -193,13 +193,40 @@ export function previewPriority(renderPriority: number, boost: number, urgentThr return Math.max(0, boosted); } -/** Creates the PREVIEW_ONLY job that follows a successful EXR render. */ +/** + * The priority an export's work runs at: the one its render was queued with. + * + * Every preview for an export derives from this rather than from a previous + * preview job, for two reasons. A preview job may not exist yet — an export + * that failed validation never had one, and reading a missing row used to + * silently drop an urgent export to normal. And deriving from an already- + * boosted preview would boost again on each retry, drifting the export up the + * queue a step at a time. + */ +export async function renderPriorityFor( + tx: Prisma.TransactionClient, + exportId: string +): Promise { + const job = await tx.renderJob.findFirst({ + where: { exportId, type: "AE_RENDER" }, + orderBy: { attempt: "desc" }, + select: { priority: true }, + }); + return job?.priority ?? 50; +} + +/** + * Creates a PREVIEW_ONLY job. `renderPriority` is the export's *render* + * priority; the queue rank is derived from it here so every path — first + * build, retry, regenerate, validation override — ranks identically. + */ export async function createPreviewJob( tx: Prisma.TransactionClient, exportRow: ExportForPreview, manifest: PreviewManifest, maxAttempts: number, - priority: number + renderPriority: number, + attempt = 1 ) { const [boost, urgentThreshold] = await Promise.all([ getConfigNumber("render.previewPriorityBoost"), @@ -210,9 +237,9 @@ export async function createPreviewJob( data: { type: "PREVIEW_ONLY", exportId: exportRow.id, - attempt: 1, + attempt, maxAttempts, - priority: previewPriority(priority, boost, urgentThreshold), + priority: previewPriority(renderPriority, boost, urgentThreshold), manifest: manifest as unknown as Prisma.InputJsonValue, }, }); @@ -421,18 +448,17 @@ export async function retryPreview(exportId: string, actorId?: string) { const last = await tx.renderJob.findFirst({ where: { exportId, type: "PREVIEW_ONLY" }, orderBy: { attempt: "desc" }, + select: { attempt: true }, }); - const job = await tx.renderJob.create({ - data: { - type: "PREVIEW_ONLY", - exportId, - attempt: (last?.attempt ?? 0) + 1, - maxAttempts, - priority: last?.priority ?? 50, - manifest: manifest as unknown as Prisma.InputJsonValue, - }, - }); + const job = await createPreviewJob( + tx, + exportRow, + manifest, + maxAttempts, + await renderPriorityFor(tx, exportId), + (last?.attempt ?? 0) + 1 + ); const updated = await applyTransition(tx, exportRow, "GENERATING_PREVIEW", { type: "USER", @@ -501,18 +527,17 @@ export async function regeneratePreview(exportId: string, input: RegeneratePrevi const last = await tx.renderJob.findFirst({ where: { exportId, type: "PREVIEW_ONLY" }, orderBy: { attempt: "desc" }, + select: { attempt: true }, }); - const job = await tx.renderJob.create({ - data: { - type: "PREVIEW_ONLY", - exportId, - attempt: (last?.attempt ?? 0) + 1, - maxAttempts, - priority: last?.priority ?? 50, - manifest: manifest as unknown as Prisma.InputJsonValue, - }, - }); + const job = await createPreviewJob( + tx, + refreshed, + manifest, + maxAttempts, + await renderPriorityFor(tx, exportId), + (last?.attempt ?? 0) + 1 + ); const updated = await applyTransition( tx, diff --git a/lib/render-pipeline/validation.ts b/lib/render-pipeline/validation.ts index 1c89157..7ae2ffe 100644 --- a/lib/render-pipeline/validation.ts +++ b/lib/render-pipeline/validation.ts @@ -4,7 +4,7 @@ import { db } from "@/lib/db"; import { PipelineError } from "./errors"; import { applyTransition } from "./transitions"; import { getConfigBoolean, getConfigNumber, getConfigValue, getPreviewConfig } from "./config"; -import { buildPreviewManifest, createPreviewJob, inheritPreviewMedia } from "./preview"; +import { buildPreviewManifest, createPreviewJob, inheritPreviewMedia, renderPriorityFor } from "./preview"; /** * Validation engine results (RenderPipeline2 §8). The worker runs the checks — @@ -301,17 +301,20 @@ export async function overrideValidation( const last = await tx.renderJob.findFirst({ where: { exportId, type: "PREVIEW_ONLY" }, orderBy: { attempt: "desc" }, + select: { attempt: true }, }); - const previewJob = await tx.renderJob.create({ - data: { - type: "PREVIEW_ONLY", - exportId, - attempt: (last?.attempt ?? 0) + 1, - maxAttempts, - priority: last?.priority ?? 50, - manifest: manifest as unknown as Prisma.InputJsonValue, - }, - }); + // An export that failed validation has never had a preview job — validation + // runs before the preview stage — so the rank comes from its render, which + // is what keeps an urgent export urgent through an override. Someone is + // usually standing over an urgent shot when they wave a check through. + const previewJob = await createPreviewJob( + tx, + exportRow, + manifest, + maxAttempts, + await renderPriorityFor(tx, exportId), + (last?.attempt ?? 0) + 1 + ); const updated = await applyTransition(tx, exportRow, "GENERATING_PREVIEW", { type: "USER",