diff --git a/src/lib/update-lock.ts b/src/lib/update-lock.ts index 2487f484..d114b959 100644 --- a/src/lib/update-lock.ts +++ b/src/lib/update-lock.ts @@ -71,20 +71,11 @@ export const readLock: LockFn = Bluebird.promisify(locker.async.readLock, { }); // Unlock all lockfiles, optionally of an appId | appUuid, then release resources. -function dispose( - release: () => void, - appIdentifier: string | number, -): Bluebird { - return Bluebird.map( - lockfile.getLocksTaken((p: string) => - p.includes(`${BASE_LOCK_DIR}/${appIdentifier}`), - ), - (lockName) => { - return lockfile.unlock(lockName); - }, - ) - .finally(release) - .return(); +async function dispose(appIdentifier: string | number): Promise { + const locks = lockfile.getLocksTaken((p: string) => + p.includes(`${BASE_LOCK_DIR}/${appIdentifier}`), + ); + await Promise.all(locks.map((l) => lockfile.unlock(l))); } /** @@ -96,69 +87,89 @@ function dispose( * TODO: Remove skipLock as it's not a good interface. If lock is called it should try to take the lock * without an option to skip. */ -export function lock( +export async function lock( appId: number, { force = false, skipLock = false }: { force: boolean; skipLock?: boolean }, fn: () => Resolvable, -): Bluebird { +): Promise { if (skipLock || appId == null) { - return Bluebird.resolve(fn()); + return Promise.resolve(fn()); } - const takeTheLock = () => { - return config - .get('lockOverride') - .then((lockOverride) => { - return writeLock(appId) - .tap((release: () => void) => { - const lockDir = getPathOnHost(lockPath(appId)); - return Bluebird.resolve(fs.readdir(lockDir)) - .catchReturn(ENOENT, []) - .mapSeries((serviceName) => { - return Bluebird.mapSeries( - lockFilesOnHost(appId, serviceName), - (tmpLockName) => { - return ( - Bluebird.try(() => { - if (force || lockOverride) { - return lockfile.unlock(tmpLockName); - } - }) - .then(() => { - return lockfile.lock(tmpLockName, LOCKFILE_UID); - }) - // If lockfile exists, throw a user-friendly error. - // Otherwise throw the error as-is. - // This will interrupt the call to Bluebird.using, so - // dispose needs to be called even though it's referenced - // by .disposer later. - .catch((error) => { - return dispose(release, appId).throw( - lockfile.LockfileExistsError.is(error) - ? new UpdatesLockedError( - `Lockfile exists for ${JSON.stringify({ - serviceName, - appId, - })}`, - ) - : (error as Error), - ); - }) - ); - }, - ); - }); - }) - .disposer((release: () => void) => dispose(release, appId)); - }) - .catch((err) => { - throw new InternalInconsistencyError( - `Error getting lockOverride config value: ${err?.message ?? err}`, - ); - }); - }; + const lockDir = getPathOnHost(lockPath(appId)); + let lockOverride: boolean; + try { + lockOverride = await config.get('lockOverride'); + } catch (err) { + throw new InternalInconsistencyError( + `Error getting lockOverride config value: ${err?.message ?? err}`, + ); + } - const disposer = takeTheLock(); + let release; + try { + // Acquire write lock for appId + release = await writeLock(appId); + // Get list of service folders in lock directory + let serviceFolders: string[] = []; + try { + serviceFolders = await fs.readdir(lockDir); + } catch (err) { + if (ENOENT(err)) { + // Default to empty list of folders + serviceFolders = []; + } else { + throw err; + } + } - return Bluebird.using(disposer, fn as () => PromiseLike); + // Attempt to create a lock for each service + await Promise.all( + serviceFolders.map((service) => + lockService(appId, service, { force, lockOverride }), + ), + ); + + // Resolve the function passed + return Promise.resolve(fn()); + } finally { + // Cleanup locks + if (release) { + release(); + } + await dispose(appId); + } +} + +type LockOptions = { + force?: boolean; + lockOverride?: boolean; +}; + +async function lockService( + appId: number, + service: string, + opts: LockOptions = { + force: false, + lockOverride: false, + }, +): Promise { + const serviceLockFiles = lockFilesOnHost(appId, service); + for await (const file of serviceLockFiles) { + try { + if (opts.force || opts.lockOverride) { + await lockfile.unlock(file); + } + await lockfile.lock(file, LOCKFILE_UID); + } catch (e) { + if (lockfile.LockfileExistsError.is(e)) { + // Throw more descriptive error + throw new UpdatesLockedError( + `Lockfile exists for { appId: ${appId}, service: ${service} }`, + ); + } + // Otherwise just throw the error + throw e; + } + } }