LockManager.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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(key, lockValue, 'EX', this.LOCK_TTL, 'NX', function (
  51. err,
  52. gotLock
  53. ) {
  54. if (err != null) {
  55. return callback(err)
  56. }
  57. if (gotLock === 'OK') {
  58. metrics.inc('doc-not-blocking')
  59. const timeTaken = profile.log('got lock').end()
  60. if (timeTaken > MAX_REDIS_REQUEST_LENGTH) {
  61. // took too long, so try to free the lock
  62. return LockManager.releaseLock(doc_id, lockValue, function (
  63. err,
  64. result
  65. ) {
  66. if (err != null) {
  67. return callback(err)
  68. } // error freeing lock
  69. return callback(null, false)
  70. }) // tell caller they didn't get the lock
  71. } else {
  72. return callback(null, true, lockValue)
  73. }
  74. } else {
  75. metrics.inc('doc-blocking')
  76. profile.log('doc is locked').end()
  77. return callback(null, false)
  78. }
  79. })
  80. },
  81. getLock(doc_id, callback) {
  82. let attempt
  83. if (callback == null) {
  84. callback = function (error, lockValue) {}
  85. }
  86. const startTime = Date.now()
  87. let testInterval = LockManager.LOCK_TEST_INTERVAL
  88. const profile = new Profiler('getLock', { doc_id })
  89. return (attempt = function () {
  90. if (Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME) {
  91. const e = new Error('Timeout')
  92. e.doc_id = doc_id
  93. profile.log('timeout').end()
  94. return callback(e)
  95. }
  96. return LockManager.tryLock(doc_id, function (error, gotLock, lockValue) {
  97. if (error != null) {
  98. return callback(error)
  99. }
  100. profile.log('tryLock')
  101. if (gotLock) {
  102. profile.end()
  103. return callback(null, lockValue)
  104. } else {
  105. setTimeout(attempt, testInterval)
  106. // back off when the lock is taken to avoid overloading
  107. return (testInterval = Math.min(
  108. testInterval * 2,
  109. LockManager.MAX_TEST_INTERVAL
  110. ))
  111. }
  112. })
  113. })()
  114. },
  115. checkLock(doc_id, callback) {
  116. if (callback == null) {
  117. callback = function (err, isFree) {}
  118. }
  119. const key = keys.blockingKey({ doc_id })
  120. return rclient.exists(key, function (err, exists) {
  121. if (err != null) {
  122. return callback(err)
  123. }
  124. exists = parseInt(exists)
  125. if (exists === 1) {
  126. metrics.inc('doc-blocking')
  127. return callback(null, false)
  128. } else {
  129. metrics.inc('doc-not-blocking')
  130. return callback(null, true)
  131. }
  132. })
  133. },
  134. releaseLock(doc_id, lockValue, callback) {
  135. const key = keys.blockingKey({ doc_id })
  136. const profile = new Profiler('releaseLock', { doc_id, key, lockValue })
  137. return rclient.eval(LockManager.unlockScript, 1, key, lockValue, function (
  138. err,
  139. result
  140. ) {
  141. if (err != null) {
  142. return callback(err)
  143. } else if (result != null && result !== 1) {
  144. // successful unlock should release exactly one key
  145. profile.log('unlockScript:expired-lock').end()
  146. logger.error(
  147. { doc_id, key, lockValue, redis_err: err, redis_result: result },
  148. 'unlocking error'
  149. )
  150. metrics.inc('unlock-error')
  151. return callback(new Error('tried to release timed out lock'))
  152. } else {
  153. profile.log('unlockScript:ok').end()
  154. return callback(null, result)
  155. }
  156. })
  157. }
  158. }