LockManager.js 1.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  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, concurrencyLimitDryRun = true) {
  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(concurrencyLimitDryRun)
  21. const lock = new Lock(key)
  22. LOCKS.set(key, lock)
  23. return lock
  24. }
  25. function checkConcurrencyLimit(dryRun) {
  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. if (!dryRun) {
  32. throw new Errors.TooManyCompileRequestsError(
  33. 'too many concurrent compile requests'
  34. )
  35. }
  36. }
  37. class Lock {
  38. constructor(key) {
  39. this.key = key
  40. this.expiresAt = Date.now() + LOCK_TIMEOUT_MS
  41. }
  42. isExpired() {
  43. return Date.now() >= this.expiresAt
  44. }
  45. release() {
  46. const lockWasActive = LOCKS.delete(this.key)
  47. if (!lockWasActive) {
  48. logger.error({ key: this.key }, 'Lock was released twice')
  49. }
  50. if (this.isExpired()) {
  51. Metrics.inc('compile_lock_expired_before_release')
  52. }
  53. }
  54. }
  55. module.exports = { acquire }