fix(producer): try Windows junctions before copying cached frames - #3740
Conversation
Follow up Anton Sidorov’s junction staging contribution in #2314 on current main. Co-authored-by: Anton Sidorov aka anticodeguy <a@anticodeguy.com>
terencecho
left a comment
There was a problem hiding this comment.
Approving at 655553e714e1e307f7868979aca7b8645066f354.
3-tier fallback (linkOrCopyFrameDir in packages/producer/src/services/render/shared.ts). Tier 1 symlinkSync(src, dest, "dir") runs everywhere; catches only EPERM/EACCES/UNKNOWN, rethrows everything else. Tier 2 tryWindowsFrameJunction is a no-op on non-Windows and attempts symlinkSync(src, dest, "junction") on Windows, treating 7 capability codes (EPERM/EACCES/UNKNOWN/EINVAL/ENOSYS/EOPNOTSUPP/ENOTSUP) as "junction unavailable → false" so tier 3 cpSync runs. Every non-capability junction error propagates — the ENOSPC test asserts cpSync is not called in that path.
Caller regression fix. renderOrchestrator.ts local executeRenderPipeline now hardcodes materializeSymlinks: false (previously shouldCopyExtractedFrames(process.platform), which was true on Windows and bypassed the link fallback entirely). the actual local Windows renderer does not request eager frame copies fakes process.platform = "win32" and verifies through executeRenderJob that the stage receives materializeSymlinks: false. That's the caller-path regression closed.
DISTRIBUTED isolation preserved. distributed materialization stays physically self-contained after cache removal passes materializeSymlinks: true explicitly, asserts the result is not a symlink, then rmSync(source, {recursive:true}) and reads a frame — bytes survive. The typecheck-green removal of the shouldCopyExtractedFrames export confirms no other caller is on the ambiguous per-platform toggle.
Junction removal preserves source frames. Real-Windows test creates the junction after the simulated symlink denial, writes updated bytes into the source, rmSync(outputDir, {recursive:true}), then reads the source file — updated bytes present. Node's rmSync({recursive:true}) on a reparse-point root unlinks the junction rather than traversing it, matching the "Tests on windows-latest: studio-engine-cli" CI run (none skipped).
Nit, non-blocking. EXDEV isn't in the junction capability-error whitelist — cross-volume extracted-cache vs compiled dir would surface as a hard error rather than degrading to copy. Rare in practice (both live under the render root), but a one-word add for anyone whose cache is on a different drive.
CI is fully green at head: Windows render verification (all lanes incl. real-junction studio-engine-cli), producer unit + integration, all 9 regression shards, typecheck, lint, format, CLI smoke, SDK contract.
— Review by tai (pr-review)
Local Windows renders currently force a recursive copy of every extracted frame, bypassing the existing link fallback. Let local renders use directory symlinks, then a Windows junction after a privilege denial, then copy if junctions are unavailable. Distributed plans continue requesting physical copies. Disk errors and stale-destination recovery retain their existing behavior.
Successor to @sidorovanthon’s #2314, with co-author credit. Junction attempts are explicitly Windows-only, preserving POSIX fallback behavior.
Validation: a regression running the actual
executeRenderJobcall failed before because Windows requested eager copying; it passes after. Locally, 226 tests pass, with the real-Windows junction case skipped on macOS. Producer typecheck, lint/format and classification pass. The targeted Windows CI step runs a real junction test: only the first symlink privilege denial is simulated, frame reads use the actual junction, and deleting the junction must preserve the source. A separate test verifies distributed copies remain readable after removing their source cache. Windows CI is required before merge.