opencode-config/docs/decisions/075-pr-178-draw-image-race-condition-tmp-render-svg.md
Sergey 39de98dc95
fix(draw-image): race condition on shared .tmp-render.svg breaks parallel renders (#178)
* fix(draw-image): use pid-based tmp svg and cleanup in finally

* test(draw-image): decouple e2e.optional from leftover tmp svg

* test(draw-image): add cleanup unit test for tmp svg lifecycle

* docs(draw-image): add handoff and ADR for tmp svg race fix

* docs(handoff): set PR number

* docs(project-map): update after structural changes

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-31 21:38:54 +03:00

2.9 KiB
Raw Blame History

ADR-075: Race condition on shared .tmp-render.svg breaks parallel renders

Статус

Accepted (2026-07-31)

Контекст

cli.ts рендерил SVG-обложки через двухфазный процесс:

  1. writeFileSync(path.join(__dirname, ".tmp-render.svg"), svg) — запись в единственный fixed-path файл в каталоге пакета.
  2. spawnSync("node", ["render.mjs", tmpSvg, outPath]) — дочерний процесс читает файл и рендерит PNG через sharp.

При параллельном запуске нескольких CLI-процессов (vitest concurrently запускает несколько test-файлов, каждый вызывает runCli через spawnSync) процессы разделяли один .tmp-render.svg: один процесс обнулял/перезаписывал файл, пока другой его читал → render.mjs падал с Input Buffer is empty (пустой буфер SVG). Дополнительно leftover .tmp-render.svg от прошлых ручных прогонов засорял git status и мог содержать неактуальный SVG, вызывая ложные падения тестов.

Решение

Использовать уникальный temp-файл с PID в имени под системным os.tmpdir():

const tmpSvg = path.join(os.tmpdir(), `draw-image-${process.pid}.svg`)

и удалять его в finally-блоке после рендера (cleanup гарантирован даже при process.exit(1)). render.mjs не изменён — он уже принимает path к SVG как argv, уникальность обеспечивает cli.ts. .gitignore уже содержал .tmp-render.svg для обратной совместимости.

Альтернативы

  • Передача SVG через stdin в render.mjs: избегает temp-файла целиком, но требует переписывания render.mjs (чтение из stdin вместо readFileSync(argv)), что выходит за рамки спеки #160 и ломает контракт «render.mjs принимает path как argv».
  • Уникальный temp-каталог на процесс вместо PID-суффикса: fs.mkdtempSync(path.join(os.tmpdir(), "draw-image-")) — даёт больше изоляции, но избыточен для одного SVG-файла; PID-суффикс достаточен, т.к. spawnSync синхронный и один процесс не запускает два рендера одновременно.
  • Atomic write (write-rename): writeFileSync(tmp + ".part") + rename(tmp + ".part", tmp) — не решает race (два процесса всё равно пишут в один путь), нужен уникальный путь.