Уже второе утро Mac зависал при загрузке. Индикатор доходил почти до конца и замирал. Я подумал, что закончилось место на диске. В режиме восстановления выяснилось обратное: свободны были 264 ГБ.
Причина оказалась во временном каталоге. За 16 часов ночные тесты оставили там 30 182 папки и 1 378 905 файлов. Сколько из них нужно, чтобы сломать загрузку macOS, я не знаю. Но с этим каталогом мой Mac не загружался, а после его очистки загрузился.
Где лежали эти файлы
На моём Mac os.tmpdir() указывал не на привычный /tmp, а на пользовательский каталог вида /private/var/folders/<xx>/<hash>/T. Документация Node.js описывает os.tmpdir() как путь к системному временному каталогу; на него также может влиять переменная TMPDIR. Поэтому путь, который возвращает функция, лучше проверять на конкретной машине.
В режиме восстановления я разблокировал том с данными, отвёл старые временные каталоги в сторону и после успешной загрузки посчитал содержимое папки T:
Источник | Папок | Файлов |
|---|---|---|
Временные файлы моих тестов | 25 299 | 1 189 858 |
Временные каталоги Vitest | 4 603 | 151 679 |
Приложения и система | 280 | 37 368 |
Всего | 30 182 | 1 378 905 |
Все эти файлы появились между 23:00 воскресенья и 15:00 понедельника. Основную массу дали несколько типов тестовых каталогов: construct-detach-* — около 246 тысяч файлов, construct-attach-* — около 234 тысяч, ghosts-preflight-test-* — ещё около 113 тысяч.
Первая попытка исправления сделала хуже
Сначала я в режиме восстановления переименовал var/folders и tmp в каталоги с суффиксом .old, затем создал пустой tmp. Я хотел убрать накопившиеся файлы с пути загрузки, сохранив старые каталоги для разбора. После перезагрузки Mac попал в цикл и по-прежнему не запускался.
Вот команды той неудачной попытки; пути относятся к моему тому данных в Recovery:
mv /Volumes/Data/private/var/folders /Volumes/Data/private/var/folders.old
mv /Volumes/Data/private/tmp /Volumes/Data/private/tmp.old
mkdir -m 1777 /Volumes/Data/private/tmpЯ упустил родительский каталог /private/var/folders. В моём случае macOS создавала пользовательские папки внутри него, но не восстановила сам каталог после переименования. Пришлось снова зайти в режим восстановления, создать пустой /private/var/folders и вернуть ему владельца root:wheel и права 755. После этого система загрузилась.
mkdir /Volumes/Data/private/var/folders
chown root:wheel /Volumes/Data/private/var/folders
chmod 755 /Volumes/Data/private/var/foldersЭто важная деталь именно этой истории: перенос системного каталога без восстановления его родительской структуры добавил мне вторую неисправность поверх первой.
Откуда взялось почти полтора миллиона файлов
Ночью агенты запускали тесты параллельно в разных git worktree. Каждый отдельный прогон выглядел нормально. Вместе они многократно повторили утечку временных файлов.
Часть тестов создавала каталог через mkdtempSync:
const dir = mkdtempSync(join(tmpdir(), 'construct-detach-'))Сам вызов создаёт каталог и возвращает его путь. Удаление нужно организовать отдельно. В тестах его не было. Один каталог легко не заметить; тысячи прогонов оставили 25 299 каталогов только от наших тестовых фикстур.
Я уже внёс исправление: каждый запуск получал собственный корневой каталог вида mkr-<pid>-XXXXXX, а TMPDIR указывал внутрь него. В конце запуска корень удалялся. Изменение попало в main в 00:50 понедельника, но утечка прекратилась только около 04:00.
Причина задержки — старые worktree. Они были созданы до исправления и продолжали выполнять прежний код. Когда я проверил их, два ещё не были обновлены. Правка в main сама по себе не меняет уже запущенные ветки.
Оставался второй источник: Vitest 5.0.0. В моих запусках он создавал временную папку при старте, до того как обёртка переназначала TMPDIR. Если процесс успевал нормально завершиться, внутреннее удаление убирало папки проектов, но корневая папка оставалась. Я воспроизвёл это на одном тестовом файле: после запуска в пустом временном каталоге появилась новая папка. Таких папок накопилось 4 603; они продолжали появляться примерно по 40 в час.
Что я изменил после разбора
Я меняю запуск Vitest: он должен проходить через ту же обёртку, что и остальные процессы, чтобы TMPDIR указывал внутрь каталога запуска ещё до старта тестового раннера. Тогда временные файлы будут удаляться вместе с этим каталогом.
На случай прерванных запусков я расширяю процесс очистки: он должен находить осиротевшие каталоги и оставленные Vitest папки старше часа. Добавляю lint-правило, которое запретит прямое создание временных каталогов через mkdtempSync(join(tmpdir(), …)) в тестах: для них предусмотрен каталог конкретного запуска. Старые worktree нужно обновлять или удалять, иначе они снова запустят код с утечкой. Поведение Vitest я также планирую описать в issue проекта.
Все мои обычные индикаторы тогда были зелёными: свободное место и память оставались в норме. Я не считал количество файлов во временной папке. После этого случая для меня у каждого созданного файла должны быть понятны владелец, срок жизни и способ удаления, который можно проверить. Путь из tmpdir() сам по себе ничего из этого не гарантирует.
Источники
Исходная статья на английском — Two hundred and sixty-four gigabytes free. Журнал инцидента, подсчёт файлов, шаги восстановления и разбор двух источников утечки.
Документация Node.js —
os.tmpdir(). Описание системного временного каталога и влияния переменнойTMPDIR.Документация Node.js —
fs.mkdtempSync(). Поведение функции создания временного каталога.
![[Перевод] Почему мой Mac не загружался, хотя на диске было свободно 264 ГБ?](https://habrastorage.org/extt/20/f4/13/20f4131ecf6300e1178eda8e49f6a243.png)
Комментарии 0