LockManager.js 1.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. const logger = require('@overleaf/logger')
  2. const Errors = require('./Errors')
  3. const RequestParser = require('./RequestParser')
  4. const Metrics = require('@overleaf/metrics')
  5. const Settings = require('@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. function acquire(key) {
  11. const currentLock = LOCKS.get(key)
  12. if (currentLock != null) {
  13. if (currentLock.isExpired()) {
  14. logger.warn({ key }, 'Compile lock expired')
  15. currentLock.release()
  16. } else {
  17. throw new Errors.AlreadyCompilingError('compile in progress')
  18. }
  19. }
  20. checkConcurrencyLimit()
  21. const lock = new Lock(key)
  22. LOCKS.set(key, lock)
  23. return lock
  24. }
  25. function checkConcurrencyLimit() {
  26. Metrics.gauge('concurrent_compile_requests', LOCKS.size)
  27. if (LOCKS.size <= Settings.compileConcurrencyLimit) {
  28. return
  29. }
  30. Metrics.inc('exceeded-compilier-concurrency-limit')
  31. throw new Errors.TooManyCompileRequestsError(
  32. 'too many concurrent compile requests'
  33. )
  34. }
  35. class Lock {
  36. constructor(key) {
  37. this.key = key
  38. this.expiresAt = Date.now() + LOCK_TIMEOUT_MS
  39. }
  40. isExpired() {
  41. return Date.now() >= this.expiresAt
  42. }
  43. release() {
  44. const lockWasActive = LOCKS.delete(this.key)
  45. if (!lockWasActive) {
  46. logger.error({ key: this.key }, 'Lock was released twice')
  47. }
  48. if (this.isExpired()) {
  49. Metrics.inc('compile_lock_expired_before_release')
  50. }
  51. }
  52. }
  53. module.exports = { acquire }