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分三步完成。

  1. 该文件是open()编辑。 这是一个primefaces操作,使用提供的标志,可以在磁盘上创build一个空文件,或者如果该文件存在,则将其截断。 截断文件是一个简单的方法,以确保只有你写的内容最终在文件中。 如果文件中存在数据,并且该文件长于随后写入文件的数据,则额外的数据将保留。 为了避免这个你截断。
  2. 内容被写入。 因为我写了这样一个简短的string,这是通过一个pwrite()调用来完成的,但是对于大量的数据,我认为nodejs一次只能写一个块。
  3. 手柄closures。

我的strace每个步骤都发生在不同的节点IO线程上。 这表明fs.writeFile()实际上可以用fs.open()fs.write()fs.close() 。 因此,nodejs并不把这个复杂的操作看作是任何级别的primefaces操作,因为它不是。 因此,如果您的节点进程在不等待操作完成的情况下终止(即使是优雅的),操作可以在上述任何步骤中进行。 在你的情况下,你看到你的进程在writeFile()完成步骤1之后,但在完成步骤2之前退出。

用POSIX层事务性地replace文件内容的常见模式是使用以下步骤:

  1. 将数据写入一个不同名称的文件。
  2. 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挂载吗?