fix(render+studio): dev mock worker (unstick the queue) + lock predefined layers
Render — "stuck in Queued" fix: - Jobs were created Queued and only a Windows AE node could claim them, so in the dev stack (no node) they queued forever. - New devworker package: in-process mock worker drives Queued jobs through the steps with progress + live preview frames → Done. Enabled via RENDER_DEV_WORKER (default true in compose; set false in prod where real nodes claim jobs). - db: DevClaimNextQueued (atomic oldest-queued → Preparing) + UpdateJobStepProgress - Verified live: a stuck job advanced Preparing→Done in ~10s with frontend polling. Studio — predefined template structure: - Projects are always copied from a template; structure is fixed. Users customise existing layers, they don't add new ones. - New studio-config flag ALLOW_ADD_LAYERS (false): StudioToolbar (add text/image/ video/shape) returns null; SceneEditSidebar "add text layer" button hidden. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -464,6 +464,61 @@ func (s *Store) CompleteJob(ctx context.Context, jobID uuid.UUID, exportID *uuid
|
||||
return s.getJobByIDInternal(ctx, jobID)
|
||||
}
|
||||
|
||||
// DevClaimNextQueued atomically picks the oldest Queued job and moves it to
|
||||
// Preparing (started_at set). Returns (nil, nil) when the queue is empty.
|
||||
//
|
||||
// This is the dev/mock path: it claims WITHOUT assigning a render node, so the
|
||||
// in-process dev worker can simulate a render end-to-end with no Windows AE node.
|
||||
// Never enabled in production (gated by RENDER_DEV_WORKER).
|
||||
func (s *Store) DevClaimNextQueued(ctx context.Context) (uuid.UUID, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
var jobID uuid.UUID
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT id FROM render.render_jobs
|
||||
WHERE step = 'Queued'::render_step
|
||||
ORDER BY priority_score DESC, queued_at ASC
|
||||
LIMIT 1 FOR UPDATE SKIP LOCKED`).Scan(&jobID)
|
||||
if err != nil {
|
||||
if err.Error() == "no rows in result set" {
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, `
|
||||
UPDATE render.render_jobs SET
|
||||
step = 'Preparing'::render_step,
|
||||
started_at = COALESCE(started_at, NOW()),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`, jobID)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
return jobID, nil
|
||||
}
|
||||
|
||||
// UpdateJobStepProgress sets the step + progress for a job (dev worker + future
|
||||
// fine-grained progress). No-op on terminal jobs.
|
||||
func (s *Store) UpdateJobStepProgress(ctx context.Context, jobID uuid.UUID, step string, progress int) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE render.render_jobs SET
|
||||
step = $1::render_step,
|
||||
render_progress = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $3
|
||||
AND step NOT IN ('Done'::render_step, 'Failed'::render_step, 'Cancelled'::render_step)`,
|
||||
step, progress, jobID)
|
||||
return err
|
||||
}
|
||||
|
||||
// FailJob marks a render job as Failed. Returns the updated job.
|
||||
func (s *Store) FailJob(ctx context.Context, jobID uuid.UUID, reason, atStep string) (*models.RenderJob, error) {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
|
||||
Reference in New Issue
Block a user