Files
flatrender/services/render/internal/devworker/devworker.go
T
soroush.asadi 43d0e10543 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>
2026-06-05 22:10:05 +03:30

132 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package devworker runs an in-process mock render worker so the full render
// flow works in development without a Windows After Effects node.
//
// When enabled (RENDER_DEV_WORKER=true) it polls for Queued jobs, claims the
// oldest, and drives it through the render steps — emitting progress and live
// preview frames exactly like a real node agent would — then marks it Done.
//
// It is NEVER started in production; real render nodes claim jobs over the
// internal API instead.
package devworker
import (
"bytes"
"context"
"encoding/base64"
"image"
"image/color"
"image/draw"
"image/png"
"log"
"time"
"github.com/flatrender/render-svc/internal/db"
"github.com/google/uuid"
)
type Worker struct {
store *db.Store
interval time.Duration
}
func New(store *db.Store) *Worker {
return &Worker{store: store, interval: 2 * time.Second}
}
// Run blocks until ctx is cancelled, processing one job at a time.
func (w *Worker) Run(ctx context.Context) {
log.Printf("[devworker] mock render worker started (poll %s) — NOT for production", w.interval)
ticker := time.NewTicker(w.interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
log.Printf("[devworker] stopped")
return
case <-ticker.C:
w.tick(ctx)
}
}
}
func (w *Worker) tick(ctx context.Context) {
jobID, err := w.store.DevClaimNextQueued(ctx)
if err != nil {
log.Printf("[devworker] claim error: %v", err)
return
}
if jobID == uuid.Nil {
return // queue empty
}
log.Printf("[devworker] claimed job %s — simulating render", jobID)
w.simulate(ctx, jobID)
}
// simulate drives a single job through the render steps with progress + preview.
func (w *Worker) simulate(ctx context.Context, jobID uuid.UUID) {
steps := []struct {
step string
pct int
}{
{"Preparing", 5},
{"TemplateCache", 15},
{"JsxGen", 25},
{"Rendering", 40},
{"Rendering", 60},
{"Rendering", 80},
{"Optimisation", 88},
{"Video", 92},
{"Uploading", 97},
}
for _, s := range steps {
select {
case <-ctx.Done():
return
case <-time.After(1200 * time.Millisecond):
}
if err := w.store.UpdateJobStepProgress(ctx, jobID, s.step, s.pct); err != nil {
// Job was cancelled / deleted mid-flight — stop quietly.
log.Printf("[devworker] job %s no longer updatable (%v) — abandoning", jobID, err)
return
}
_ = w.store.UpdateJobPreview(ctx, jobID, previewB64(s.pct))
log.Printf("[devworker] job %s — %s %d%%", jobID, s.step, s.pct)
}
// Complete (no export — dev renders produce no downloadable artifact).
if _, err := w.store.CompleteJob(ctx, jobID, nil); err != nil {
log.Printf("[devworker] complete job %s failed: %v", jobID, err)
return
}
log.Printf("[devworker] job %s done", jobID)
}
// previewB64 builds a 320×180 PNG with a progress bar — same idea as the node
// agent's GeneratePreviewB64, kept local so render-svc has no node-agent dep.
func previewB64(pct int) string {
const w, h = 320, 180
img := image.NewRGBA(image.Rect(0, 0, w, h))
draw.Draw(img, img.Bounds(), &image.Uniform{color.RGBA{15, 17, 30, 255}}, image.Point{}, draw.Src)
barY, barH := h/2-6, 12
draw.Draw(img, image.Rect(20, barY, w-20, barY+barH),
&image.Uniform{color.RGBA{30, 34, 56, 255}}, image.Point{}, draw.Src)
if pct > 0 {
fillW := int(float64(w-40) * float64(pct) / 100.0)
if fillW < 2 {
fillW = 2
}
r := uint8(76 - int(float64(pct)*0.3))
g := uint8(110 + int(float64(pct)*0.8))
b := uint8(245 - int(float64(pct)*1.3))
draw.Draw(img, image.Rect(20, barY, 20+fillW, barY+barH),
&image.Uniform{color.RGBA{r, g, b, 255}}, image.Point{}, draw.Src)
}
var buf bytes.Buffer
_ = png.Encode(&buf, img)
return base64.StdEncoding.EncodeToString(buf.Bytes())
}