* 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>
26 lines
No EOL
2.9 KiB
Markdown
26 lines
No EOL
2.9 KiB
Markdown
# 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()`:
|
||
```ts
|
||
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 (два процесса всё равно пишут в один путь), нужен уникальный путь. |