Проблема
fs.writeFile
не является атомарной операцией. Вот пример программы, которую я буду запускать strace
:
#!/usr/bin/env node
const { writeFile, } = require('fs');
// nodejs won’t exit until the Promise completes.
new Promise(function (resolve, reject) {
writeFile('file.txt', 'content\n', function (err) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
Когда я запускаю это под strace -f
и убираю вывод, чтобы показать только системные вызовы из операции writeFile
(, которая фактически охватывает несколько потоков ввода-вывода), я получаю:
open("file.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 9
pwrite(9, "content\n", 8, 0) = 8
close(9) = 0
Как видите, writeFile
выполняется в три этапа.
- Это файл
open()
ed. Это атомарная операция, которая с предоставленными флагами либо создает пустой файл на диске, либо, если файл существует, обрезает его. Усечение файла - это простой способ убедиться, что в файл попадает только написанное вами содержимое. Если в файле есть существующие данные и файл длиннее, чем данные, которые вы впоследствии записываете в файл, дополнительные данные останутся. Чтобы избежать этого, вы обрезаете.
- Содержание написано. Поскольку я написал такую короткую строку, это делается с помощью одного _9 _, но для больших объемов данных я предполагаю, что возможно, что nodejs будет записывать только фрагмент за раз.
- Ручка закрыта.
В моем strace
каждый из этих шагов выполнялся в другом потоке ввода-вывода узла. Это наводит на мысль, что fs.writeFile()
на самом деле может быть реализовано в терминах fs.open()
, _ 13_ и _ 14_. Таким образом, nodejs не рассматривает эту сложную операцию как атомарную на любом уровне - потому что это не так. Поэтому, если процесс вашего узла завершается, даже изящно, не дожидаясь завершения операции, операция может быть выполнена на любом из описанных выше шагов. В вашем случае вы видите, что процесс завершается после того, как writeFile()
завершит шаг 1, но до того, как он завершит шаг 2.
Решение
Распространенный шаблон для транзакционной замены содержимого файла слоем POSIX заключается в использовании следующих шагов:
- Запишите данные в файл с другим именем,
fsync()
файл (см. «Когда следует использовать fsync?» В «Обеспечение попадания данных на диск»), а затем close()
это.
rename()
(или, в Windows, _ 19_ с MOVEFILE_REPLACE_EXISTING
) файл с другим именем над тем, который вы хотите заменить.
Используя этот алгоритм, целевой файл либо обновляется, либо нет, независимо от того, когда ваша программа завершается. И, что еще лучше, журналируемые (современные) файловые системы гарантируют, что, пока вы fsync()
файл на шаге 1 перед переходом к шагу 2, две операции будут выполняться по порядку. То есть, если ваша программа выполняет шаг 1, а затем шаг 2, но вы отключаете вилку, при загрузке вы обнаружите, что файловая система находится в одном из следующих состояний:
- Ни один из двух шагов не завершен. Исходный файл не поврежден (или, если его раньше не было, то не существует). Файл замены либо не существует (шаг 1 алгоритма
writeFile()
, open()
, фактически никогда не был успешным), существует, но пуст (шаг 1 алгоритма writeFile()
завершен), либо существует с некоторыми данными (шаг 2 алгоритма writeFile()
частично завершен).
- Первый шаг выполнен. Исходный файл не поврежден (или, если он не существовал ранее, все еще не существует). Файл замены существует со всеми необходимыми данными.
- Оба шага выполнены. По пути к исходному файлу теперь вы можете получить доступ к своим данным замены - всем, а не пустому файлу. Путь, по которому вы записали данные замены на первом шаге, больше не существует.
Код для использования этого шаблона может выглядеть следующим образом:
const { writeFile, rename, } = require('fs');
function writeFileTransactional (path, content, cb) {
// The replacement file must be in the same directory as the
// destination because rename() does not work across device
// boundaries.
// This simple choice of replacement filename means that this
// function must never be called concurrently with itself for the
// same path value. Also, properly guarding against other
// processes trying to use the same temporary path would make this
// function more complicated. If that is a concern, a proper
// temporary file strategy should be used. However, this
// implementation ensures that any files left behind during an
// unclean termination will be cleaned up on a future run.
let temporaryPath = `${path}.new`;
writeFile(temporaryPath, content, function (err) {
if (err) {
return cb(err);
}
rename(temporaryPath, path, cb);
});
};
По сути, это то же самое решение, которое вы использовали бы для той же проблемы на любом языке / фреймворке.
person
binki
schedule
21.08.2016
data.json.bak
, затем какdata.json
. один из них не будет пустым. - person dandavis   schedule 17.06.2015fs.rename()
вы можете пропустить шаги 2 и 4 и избежать состояния гонки / сбоя, когда имя назначения не существует. - person binki   schedule 08.08.2017