LockManager.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. /* eslint-disable
  2. camelcase,
  3. no-return-assign,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * DS207: Consider shorter variations of null checks
  11. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. let LockManager
  14. const metrics = require('./Metrics')
  15. const Settings = require('@overleaf/settings')
  16. const redis = require('@overleaf/redis-wrapper')
  17. const rclient = redis.createClient(Settings.redis.lock)
  18. const keys = Settings.redis.lock.key_schema
  19. const logger = require('@overleaf/logger')
  20. const os = require('os')
  21. const crypto = require('crypto')
  22. const Profiler = require('./Profiler')
  23. const HOST = os.hostname()
  24. const PID = process.pid
  25. const RND = crypto.randomBytes(4).toString('hex')
  26. let COUNT = 0
  27. const MAX_REDIS_REQUEST_LENGTH = 5000 // 5 seconds
  28. module.exports = LockManager = {
  29. LOCK_TEST_INTERVAL: 50, // 50ms between each test of the lock
  30. MAX_TEST_INTERVAL: 1000, // back off to 1s between each test of the lock
  31. MAX_LOCK_WAIT_TIME: 10000, // 10s maximum time to spend trying to get the lock
  32. LOCK_TTL: 30, // seconds. Time until lock auto expires in redis.
  33. // Use a signed lock value as described in
  34. // http://redis.io/topics/distlock#correct-implementation-with-a-single-instance
  35. // to prevent accidental unlocking by multiple processes
  36. randomLock() {
  37. const time = Date.now()
  38. return `locked:host=${HOST}:pid=${PID}:random=${RND}:time=${time}:count=${COUNT++}`
  39. },
  40. unlockScript:
  41. 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end',
  42. tryLock(doc_id, callback) {
  43. if (callback == null) {
  44. callback = function () {}
  45. }
  46. const lockValue = LockManager.randomLock()
  47. const key = keys.blockingKey({ doc_id })
  48. const profile = new Profiler('tryLock', { doc_id, key, lockValue })
  49. return rclient.set(
  50. key,
  51. lockValue,
  52. 'EX',
  53. this.LOCK_TTL,
  54. 'NX',
  55. function (err, gotLock) {
  56. if (err != null) {
  57. return callback(err)
  58. }
  59. if (gotLock === 'OK') {
  60. metrics.inc('doc-not-blocking')
  61. const timeTaken = profile.log('got lock').end()
  62. if (timeTaken > MAX_REDIS_REQUEST_LENGTH) {
  63. // took too long, so try to free the lock
  64. return LockManager.releaseLock(
  65. doc_id,
  66. lockValue,
  67. function (err, result) {
  68. if (err != null) {
  69. return callback(err)
  70. } // error freeing lock
  71. return callback(null, false)
  72. }
  73. ) // tell caller they didn't get the lock
  74. } else {
  75. return callback(null, true, lockValue)
  76. }
  77. } else {
  78. metrics.inc('doc-blocking')
  79. profile.log('doc is locked').end()
  80. return callback(null, false)
  81. }
  82. }
  83. )
  84. },
  85. getLock(doc_id, callback) {
  86. let attempt
  87. if (callback == null) {
  88. callback = function () {}
  89. }
  90. const startTime = Date.now()
  91. let testInterval = LockManager.LOCK_TEST_INTERVAL
  92. const profile = new Profiler('getLock', { doc_id })
  93. return (attempt = function () {
  94. if (Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME) {
  95. const e = new Error('Timeout')
  96. e.doc_id = doc_id
  97. profile.log('timeout').end()
  98. return callback(e)
  99. }
  100. return LockManager.tryLock(doc_id, function (error, gotLock, lockValue) {
  101. if (error != null) {
  102. return callback(error)
  103. }
  104. profile.log('tryLock')
  105. if (gotLock) {
  106. profile.end()
  107. return callback(null, lockValue)
  108. } else {
  109. setTimeout(attempt, testInterval)
  110. // back off when the lock is taken to avoid overloading
  111. return (testInterval = Math.min(
  112. testInterval * 2,
  113. LockManager.MAX_TEST_INTERVAL
  114. ))
  115. }
  116. })
  117. })()
  118. },
  119. checkLock(doc_id, callback) {
  120. if (callback == null) {
  121. callback = function () {}
  122. }
  123. const key = keys.blockingKey({ doc_id })
  124. return rclient.exists(key, function (err, exists) {
  125. if (err != null) {
  126. return callback(err)
  127. }
  128. exists = parseInt(exists)
  129. if (exists === 1) {
  130. metrics.inc('doc-blocking')
  131. return callback(null, false)
  132. } else {
  133. metrics.inc('doc-not-blocking')
  134. return callback(null, true)
  135. }
  136. })
  137. },
  138. releaseLock(doc_id, lockValue, callback) {
  139. const key = keys.blockingKey({ doc_id })
  140. const profile = new Profiler('releaseLock', { doc_id, key, lockValue })
  141. return rclient.eval(
  142. LockManager.unlockScript,
  143. 1,
  144. key,
  145. lockValue,
  146. function (err, result) {
  147. if (err != null) {
  148. return callback(err)
  149. } else if (result != null && result !== 1) {
  150. // successful unlock should release exactly one key
  151. profile.log('unlockScript:expired-lock').end()
  152. logger.error(
  153. { doc_id, key, lockValue, redis_err: err, redis_result: result },
  154. 'unlocking error'
  155. )
  156. metrics.inc('unlock-error')
  157. return callback(new Error('tried to release timed out lock'))
  158. } else {
  159. profile.log('unlockScript:ok').end()
  160. return callback(null, result)
  161. }
  162. }
  163. )
  164. },
  165. }