LockManager.js 5.4 KB

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