Files
overleaf-cep/services/clsi/app/js/LockManager.js
Jakob Ackermann 07397bbdde [clsi] avoid server error when clearing cache while compiling (#32349)
* [clsi] avoid server error when clearing cache while compiling

* [clsi] tweak API around releasing locks

Co-authored-by: Eric Mc Sween <eric.mcsween@overleaf.com>

---------

Co-authored-by: Eric Mc Sween <eric.mcsween@overleaf.com>
GitOrigin-RevId: d3f171467d3bc26941758dd333f30049b37a05c8
2026-03-23 09:06:18 +00:00

84 lines
1.9 KiB
JavaScript

import logger from '@overleaf/logger'
import Errors from './Errors.js'
import RequestParser from './RequestParser.js'
import Metrics from '@overleaf/metrics'
import Settings from '@overleaf/settings'
// The lock timeout should be higher than the maximum end-to-end compile time.
// Here, we use the maximum compile timeout plus 2 minutes.
const LOCK_TIMEOUT_MS = RequestParser.MAX_TIMEOUT * 1000 + 120000
const LOCKS = new Map()
/**
* @param key
* @return {Lock | undefined}
*/
function getExistingLock(key) {
return LOCKS.get(key)
}
function acquire(key) {
const currentLock = LOCKS.get(key)
if (currentLock != null) {
if (currentLock.isExpired()) {
logger.warn({ key }, 'Compile lock expired')
currentLock.release()
} else {
throw new Errors.AlreadyCompilingError('compile in progress')
}
}
checkConcurrencyLimit()
const lock = new Lock(key)
LOCKS.set(key, lock)
return lock
}
function checkConcurrencyLimit() {
Metrics.gauge('concurrent_compile_requests', LOCKS.size)
if (LOCKS.size <= Settings.compileConcurrencyLimit) {
return
}
Metrics.inc('exceeded-compilier-concurrency-limit')
throw new Errors.TooManyCompileRequestsError(
'too many concurrent compile requests'
)
}
class Lock {
constructor(key) {
this.key = key
this.expiresAt = Date.now() + LOCK_TIMEOUT_MS
}
isExpired() {
return Date.now() >= this.expiresAt
}
waitForRelease() {
if (this.waitingForRelease) return this.waitingForRelease
this.waitingForRelease = new Promise(resolve => {
this.onRelease = resolve
})
return this.waitingForRelease
}
release() {
if (this.onRelease) this.onRelease()
const lockWasActive = LOCKS.delete(this.key)
if (!lockWasActive) {
logger.error({ key: this.key }, 'Lock was released twice')
}
if (this.isExpired()) {
Metrics.inc('compile_lock_expired_before_release')
}
}
}
export default { acquire, getExistingLock }