diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/util.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/util.js index b91a99129..6775dcda7 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/util.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/util.js @@ -71,30 +71,31 @@ function readFile(path,backupPath,emptyResponse,type) { }); } +const writeFileLocks = {} + module.exports = { /** * Write content to a file using UTF8 encoding. * This forces a fsync before completing to ensure * the write hits disk. */ - writeFile: function(path,content,backupPath) { - var backupPromise; - if (backupPath && fs.existsSync(path)) { - backupPromise = fs.copy(path,backupPath); - } else { - backupPromise = Promise.resolve(); + writeFile: async function(path,content,backupPath) { + const reqId = Math.floor(Math.random()*1000) + if (!writeFileLocks[path]) { + writeFileLocks[path] = Promise.resolve() } - - const dirname = fspath.dirname(path); - const tempFile = `${path}.$$$`; - - return backupPromise.then(() => { - if (backupPath) { + const result = writeFileLocks[path].then(async () => { + var backupPromise; + if (backupPath && fs.existsSync(path)) { + await fs.copy(path,backupPath); log.trace(`utils.writeFile - copied ${path} TO ${backupPath}`) } - return fs.ensureDir(dirname) - }).then(() => { - return new Promise(function(resolve,reject) { + + const dirname = fspath.dirname(path); + const tempFile = `${path}.$$$`; + await fs.ensureDir(dirname) + + await new Promise(function(resolve,reject) { var stream = fs.createWriteStream(tempFile); stream.on('open',function(fd) { stream.write(content,'utf8',function() { @@ -110,10 +111,11 @@ module.exports = { log.warn(log._("storage.localfilesystem.fsync-fail",{path: tempFile, message: err.toString()})); reject(err); }); - }); - }).then(() => { + }) + log.trace(`utils.writeFile - written content to ${tempFile}`) - return new Promise(function(resolve,reject) { + + await new Promise(function(resolve,reject) { fs.rename(tempFile,path,err => { if (err) { log.warn(log._("storage.localfilesystem.fsync-fail",{path: path, message: err.toString()})); @@ -122,8 +124,10 @@ module.exports = { log.trace(`utils.writeFile - renamed ${tempFile} to ${path}`) resolve(); }) - }); - }); + }) + }) + writeFileLocks[path] = result.catch(() => {}) + return result }, readFile: readFile, diff --git a/test/unit/@node-red/runtime/lib/storage/localfilesystem/util_spec.js b/test/unit/@node-red/runtime/lib/storage/localfilesystem/util_spec.js index fa1e4bdb7..49cccb462 100644 --- a/test/unit/@node-red/runtime/lib/storage/localfilesystem/util_spec.js +++ b/test/unit/@node-red/runtime/lib/storage/localfilesystem/util_spec.js @@ -14,11 +14,33 @@ * limitations under the License. **/ -var should = require("should"); -var NR_TEST_UTILS = require("nr-test-utils"); -var util = NR_TEST_UTILS.require("@node-red/runtime/lib/storage/localfilesystem/util"); +const should = require("should"); +const NR_TEST_UTILS = require("nr-test-utils"); +const util = NR_TEST_UTILS.require("@node-red/runtime/lib/storage/localfilesystem/util"); +const { mkdtemp, readFile } = require('fs/promises'); +const { join } = require('path'); +const { tmpdir } = require('os'); describe('storage/localfilesystem/util', function() { + describe('writeFile', function () { + it('manages concurrent calls to modify the same file', async function () { + const testDirectory = await mkdtemp(join(tmpdir(), 'nr-test-')); + const testFile = join(testDirectory, 'foo.txt') + const testBackupFile = testFile + '.$$$' + + let counter = 0 + const promises = [ + util.writeFile(testFile, `update-${counter++}`, testBackupFile ), + util.writeFile(testFile, `update-${counter++}`, testBackupFile ), + util.writeFile(testFile, `update-${counter++}`, testBackupFile ) + ] + + await Promise.all(promises) + + const result = await readFile(testFile, { encoding: 'utf-8' }) + result.should.equal('update-2') + }) + }) describe('parseJSON', function() { it('returns parsed JSON', function() { var result = util.parseJSON('{"a":123}');