concurrency.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. 'use strict'
  2. process.setMaxListeners(1000000);
  3. const fs = require('fs')
  4. const {test} = require('tap')
  5. const requireInject = require('require-inject')
  6. // defining mock for fs so its functions can be modified
  7. const fsMock = Object.assign ( {}, fs, {
  8. /* ASYNC */
  9. mkdir (filename, opts, cb) {
  10. return cb(null);
  11. },
  12. realpath (filename, cb) {
  13. return cb(null, filename)
  14. },
  15. open (tmpfile, options, mode, cb) {
  16. if (/noopen/.test(tmpfile)) return cb(new Error('ENOOPEN'))
  17. cb(null, tmpfile)
  18. },
  19. write (fd) {
  20. const cb = arguments[arguments.length - 1]
  21. if (/nowrite/.test(fd)) return cb(new Error('ENOWRITE'))
  22. cb()
  23. },
  24. fsync (fd, cb) {
  25. if (/nofsync/.test(fd)) return cb(new Error('ENOFSYNC'))
  26. cb()
  27. },
  28. close (fd, cb) {
  29. cb()
  30. },
  31. chown (tmpfile, uid, gid, cb) {
  32. if (/nochown/.test(tmpfile)) return cb(new Error('ENOCHOWN'))
  33. cb()
  34. },
  35. chmod (tmpfile, mode, cb) {
  36. if (/nochmod/.test(tmpfile)) return cb(new Error('ENOCHMOD'))
  37. cb()
  38. },
  39. rename (tmpfile, filename, cb) {
  40. if (/norename/.test(tmpfile)) return cb(new Error('ENORENAME'))
  41. cb()
  42. },
  43. unlink (tmpfile, cb) {
  44. if (/nounlink/.test(tmpfile)) return cb(new Error('ENOUNLINK'))
  45. cb()
  46. },
  47. stat (tmpfile, cb) {
  48. if (/nostat/.test(tmpfile)) return cb(new Error('ENOSTAT'))
  49. cb()
  50. },
  51. /* SYNC */
  52. mkdirSync (filename) {},
  53. realpathSync (filename, cb) {
  54. return filename
  55. },
  56. openSync (tmpfile, options) {
  57. if (/noopen/.test(tmpfile)) throw new Error('ENOOPEN')
  58. return tmpfile
  59. },
  60. writeSync (fd) {
  61. if (/nowrite/.test(fd)) throw new Error('ENOWRITE')
  62. },
  63. fsyncSync (fd) {
  64. if (/nofsync/.test(fd)) throw new Error('ENOFSYNC')
  65. },
  66. closeSync () {},
  67. chownSync (tmpfile, uid, gid) {
  68. if (/nochown/.test(tmpfile)) throw new Error('ENOCHOWN')
  69. },
  70. chmodSync (tmpfile, mode) {
  71. if (/nochmod/.test(tmpfile)) throw new Error('ENOCHMOD')
  72. },
  73. renameSync (tmpfile, filename) {
  74. if (/norename/.test(tmpfile)) throw new Error('ENORENAME')
  75. },
  76. unlinkSync (tmpfile) {
  77. if (/nounlink/.test(tmpfile)) throw new Error('ENOUNLINK')
  78. },
  79. statSync (tmpfile) {
  80. if (/nostat/.test(tmpfile)) throw new Error('ENOSTAT')
  81. }
  82. })
  83. const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMock });
  84. // preserve original functions
  85. const oldRealPath = fsMock.realpath
  86. const oldRename = fsMock.rename
  87. test('ensure writes to the same file are serial', t => {
  88. let fileInUse = false
  89. const ops = 5 // count for how many concurrent write ops to request
  90. t.plan(ops * 3 + 3)
  91. fsMock.realpath = (...args) => {
  92. t.false(fileInUse, 'file not in use')
  93. fileInUse = true
  94. oldRealPath(...args)
  95. }
  96. fsMock.rename = (...args) => {
  97. t.true(fileInUse, 'file in use')
  98. fileInUse = false
  99. oldRename(...args)
  100. }
  101. const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMock });
  102. for (let i = 0; i < ops; i++) {
  103. writeFileAtomic('test', 'test', err => {
  104. if (err) t.fail(err)
  105. else t.pass('wrote without error')
  106. })
  107. }
  108. setTimeout(() => {
  109. writeFileAtomic('test', 'test', err => {
  110. if (err) t.fail(err)
  111. else t.pass('successive writes after delay')
  112. })
  113. }, 500)
  114. })
  115. test('allow write to multiple files in parallel, but same file writes are serial', t => {
  116. const filesInUse = []
  117. const ops = 5
  118. let wasParallel = false
  119. fsMock.realpath = (filename, ...args) => {
  120. filesInUse.push(filename)
  121. const firstOccurence = filesInUse.indexOf(filename)
  122. t.equal(filesInUse.indexOf(filename, firstOccurence + 1), -1, 'serial writes') // check for another occurence after the first
  123. if (filesInUse.length > 1) wasParallel = true // remember that a parallel operation took place
  124. oldRealPath(filename, ...args)
  125. }
  126. fsMock.rename = (filename, ...args) => {
  127. filesInUse.splice(filesInUse.indexOf(filename), 1)
  128. oldRename(filename, ...args)
  129. }
  130. const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMock });
  131. t.plan(ops * 2 * 2 + 1)
  132. let opCount = 0
  133. for (let i = 0; i < ops; i++) {
  134. writeFileAtomic('test', 'test', err => {
  135. if (err) t.fail(err, 'wrote without error')
  136. else t.pass('wrote without error')
  137. })
  138. writeFileAtomic('test2', 'test', err => {
  139. opCount++
  140. if (opCount === ops) t.true(wasParallel, 'parallel writes')
  141. if (err) t.fail(err, 'wrote without error')
  142. else t.pass('wrote without error')
  143. })
  144. }
  145. })