LockManager.js 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. import logger from '@overleaf/logger'
  2. import Errors from './Errors.js'
  3. import RequestParser from './RequestParser.js'
  4. import Metrics from '@overleaf/metrics'
  5. import Settings from '@overleaf/settings'
  6. // The lock timeout should be higher than the maximum end-to-end compile time.
  7. // Here, we use the maximum compile timeout plus 2 minutes.
  8. const LOCK_TIMEOUT_MS = RequestParser.MAX_TIMEOUT * 1000 + 120000
  9. const LOCKS = new Map()
  10. /**
  11. * @param key
  12. * @return {Lock | undefined}
  13. */
  14. function getExistingLock(key) {
  15. return LOCKS.get(key)
  16. }
  17. function acquire(key) {
  18. const currentLock = LOCKS.get(key)
  19. if (currentLock != null) {
  20. if (currentLock.isExpired()) {
  21. logger.warn({ key }, 'Compile lock expired')
  22. currentLock.release()
  23. } else {
  24. throw new Errors.AlreadyCompilingError('compile in progress')
  25. }
  26. }
  27. checkConcurrencyLimit()
  28. const lock = new Lock(key)
  29. LOCKS.set(key, lock)
  30. return lock
  31. }
  32. function checkConcurrencyLimit() {
  33. Metrics.gauge('concurrent_compile_requests', LOCKS.size)
  34. if (LOCKS.size <= Settings.compileConcurrencyLimit) {
  35. return
  36. }
  37. Metrics.inc('exceeded-compilier-concurrency-limit')
  38. throw new Errors.TooManyCompileRequestsError(
  39. 'too many concurrent compile requests'
  40. )
  41. }
  42. class Lock {
  43. constructor(key) {
  44. this.key = key
  45. this.expiresAt = Date.now() + LOCK_TIMEOUT_MS
  46. }
  47. isExpired() {
  48. return Date.now() >= this.expiresAt
  49. }
  50. waitForRelease() {
  51. if (this.waitingForRelease) return this.waitingForRelease
  52. this.waitingForRelease = new Promise(resolve => {
  53. this.onRelease = resolve
  54. })
  55. return this.waitingForRelease
  56. }
  57. release() {
  58. if (this.onRelease) this.onRelease()
  59. const lockWasActive = LOCKS.delete(this.key)
  60. if (!lockWasActive) {
  61. logger.error({ key: this.key }, 'Lock was released twice')
  62. }
  63. if (this.isExpired()) {
  64. Metrics.inc('compile_lock_expired_before_release')
  65. }
  66. }
  67. }
  68. export default { acquire, getExistingLock }