mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-20 06:07:57 +00:00
Refactor lock function
Previous code was callback hell Signed-off-by: 20k-ultra <3946250+20k-ultra@users.noreply.github.com>
This commit is contained in:
parent
4f1d0d58da
commit
b6a80ff479
@ -15,6 +15,7 @@ import { getPathOnHost, pathExistsOnHost } from './fs-utils';
|
||||
import * as config from '../config';
|
||||
import * as lockfile from './lockfile';
|
||||
import { NumericIdentifier } from '../types';
|
||||
import log from '../lib/supervisor-console';
|
||||
|
||||
const decodedUid = NumericIdentifier.decode(process.env.LOCKFILE_UID);
|
||||
export const LOCKFILE_UID = isRight(decodedUid) ? decodedUid.right : 65534;
|
||||
@ -61,7 +62,7 @@ export function abortIfHUPInProgress({
|
||||
});
|
||||
}
|
||||
|
||||
type LockFn = (key: string | number) => Bluebird<() => void>;
|
||||
type LockFn = (key: string | number) => Promise<() => void>;
|
||||
const locker = new Lock();
|
||||
export const writeLock: LockFn = Bluebird.promisify(locker.async.writeLock, {
|
||||
context: locker,
|
||||
@ -70,21 +71,11 @@ export const readLock: LockFn = Bluebird.promisify(locker.async.readLock, {
|
||||
context: locker,
|
||||
});
|
||||
|
||||
// Unlock all lockfiles, optionally of an appId | appUuid, then release resources.
|
||||
function dispose(
|
||||
release: () => void,
|
||||
appIdentifier: string | number,
|
||||
): Bluebird<void> {
|
||||
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<void> {
|
||||
const locks = lockfile.getLocksTaken((p: string) =>
|
||||
p.includes(`${BASE_LOCK_DIR}/${appIdentifier}`),
|
||||
);
|
||||
await Promise.all(locks.map((l) => lockfile.unlock(l)));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,74 +110,87 @@ export async function lockAll<T extends unknown>(
|
||||
* Try to take the locks for an application. If force is set, it will remove
|
||||
* all existing lockfiles before performing the operation
|
||||
*
|
||||
* TODO: convert to native Promises and async/await. May require native implementation of Bluebird's dispose / using
|
||||
*
|
||||
* 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<T extends unknown>(
|
||||
export async function lock<T extends unknown>(
|
||||
appId: number,
|
||||
{ force = false, skipLock = false }: { force: boolean; skipLock?: boolean },
|
||||
fn: () => Resolvable<T>,
|
||||
): Bluebird<T> {
|
||||
): Promise<T> {
|
||||
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 (e) {
|
||||
log.error(`Error getting lock directory contents - ${e}`);
|
||||
}
|
||||
|
||||
return Bluebird.using(disposer, fn as () => PromiseLike<T>);
|
||||
// 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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user