Node.js fs.writeFile()清空文件
我有一个更新的方法,每16-40毫秒被调用,里面我有这样的代码:
this.fs.writeFile("./data.json", JSON.stringify({ totalPlayersOnline: this.totalPlayersOnline, previousDay: this.previousDay, gamesToday: this.gamesToday }), function (err) { if (err) { return console.log(err); } });
如果服务器抛出一个错误,那么“data.json”文件有时变空。 我如何防止?
问题
fs.writeFile
不是一个primefaces操作。 这里是一个示例程序,我将运行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
操作( 实际上跨越多个IO线程 )的系统调用时,我得到:
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()
编辑。 这是一个primefaces操作,使用提供的标志,可以在磁盘上创build一个空文件,或者如果该文件存在,则将其截断。 截断文件是一个简单的方法,以确保只有你写的内容最终在文件中。 如果文件中存在数据,并且该文件长于随后写入文件的数据,则额外的数据将保留。 为了避免这个你截断。 - 内容被写入。 因为我写了这样一个简短的string,这是通过一个
pwrite()
调用来完成的,但是对于大量的数据,我认为nodejs一次只能写一个块。 - 手柄closures。
我的strace
每个步骤都发生在不同的节点IO线程上。 这表明fs.writeFile()
实际上可以用fs.open()
, fs.write()
和fs.close()
。 因此,nodejs并不把这个复杂的操作看作是任何级别的primefaces操作,因为它不是。 因此,如果您的节点进程在不等待操作完成的情况下终止(即使是优雅的),操作可以在上述任何步骤中进行。 在你的情况下,你看到你的进程在writeFile()
完成步骤1之后,但在完成步骤2之前退出。
解
用POSIX层事务性地replace文件内容的常见模式是使用以下步骤:
- 将数据写入一个不同名称的文件。
-
rename()
(或者,在Windows上,MoveFileEx()
和MOVEFILE_REPLACE_EXISTING
)不同名称的文件覆盖要replace的文件。
使用这种algorithm,无论程序何时终止,目标文件都被更新或者不更新。 而且,更好的做法是,只要在执行步骤2之前close()
第一步中的文件,两个操作就会按顺序进行。 也就是说,如果您的程序执行步骤1,然后执行步骤2,但是您拔下插头,则在启动时会发现文件系统处于以下状态之一:
- 这两个步骤都没有完成。 原始文件是完整的(或者如果它以前从未存在,则不存在)。 replace文件不存在(
writeFile()
algorithm的第1步,open()
,实际上从来没有成功),存在但是为空(writeFile()
algorithm的第1步完成),或者存在于某些数据(writeFile()
algorithm部分完成)。 - 第一步完成。 原始文件是完整的(或者如果在它不存在之前它不存在)。 replace文件存在所有你想要的数据。
- 两个步骤完成。 在原始文件的path上,现在可以访问replace数据 – 所有这些都不是空白文件。 您在第一步中写入replace数据的path不再存在。
使用此模式的代码可能如下所示:
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 tyring 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); }); };
这与您在任何语言/框架中使用相同问题的方法基本相同。
如果错误是由于input错误(要写入的数据)造成的,请确保数据正确,然后执行writeFile。 如果错误是由于写入文件失败而引起的,即使input正确,也可以检查该函数是否被执行,直到写入文件。 一种方法是使用async doWhilst函数。
async.doWhilst( writeFile(), //your function here but instead of err when fail callback success to loop again check_if_file_null, //a function that checks that the file is not null function (err) { //here the file is not null } );
我没有运行一些真正的testing,我只是注意到手动重新加载我的IDE有时文件是空的。 我首先尝试的是重命名方法,并指出了同样的问题,但重新创build一个新文件是不太理想的(考虑文件手表等)。
我的build议或我现在正在做的是在你自己的readFileSync我检查文件是否丢失或返回的数据是空的睡觉100毫秒,然后再次尝试。 我想假设再延迟三分之一会让西格玛上升一个档次,但是现在不会这么做,因为延迟时间会带来一些不必要的负面影响(会考虑当时的承诺)。 还有其他恢复选项的机会相对于你自己的代码,你可以添加,以防万一,我希望。 文件未find或为空? 基本上是另一种方式的重试。
我的自定义writeFileSync有一个添加标志,可以在使用重命名方法(使用写入子目录“._new”创build)或普通直接方法之间切换,因为您的代码的需求可能会有所不同。 我的build议可能基于文件大小。
在这种用例中,文件很小,一次只能由一个节点实例/服务器更新。 我可以看到添加随机文件名称作为另一个选项与重命名,以便允许多个机器写入另一个选项,如果需要以后。 也许是一个重试限制的论据?
我也在想,你可以写一个本地临时文件,然后通过某种方法复制共享目标(也许还可以在目标上重命名以提高速度),然后清理(取消与当地temp的链接)当然。 我想这个想法是把它推到shell命令,所以不是更好。 无论如何,仍然主要的想法是读两次,如果发现空的。 我敢肯定它是安全的从部分写入,通过nodejs 8+到共享的Ubuntutypes的NFS挂载吗?