refactor: Convert update-lock module to typescript

Change-type: patch
Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
Cameron Diver 2018-12-13 14:01:34 +00:00
parent ec37db597d
commit b977b30dfe
No known key found for this signature in database
GPG Key ID: 49690ED87032539F
6 changed files with 110 additions and 74 deletions

View File

@ -34,6 +34,7 @@
"@types/event-stream": "^3.3.34",
"@types/express": "^4.11.1",
"@types/knex": "^0.14.14",
"@types/lockfile": "^1.0.0",
"@types/lodash": "^4.14.109",
"@types/memoizee": "^0.4.2",
"@types/mz": "0.0.32",

View File

@ -17,7 +17,7 @@ validation = require './lib/validation'
systemd = require './lib/systemd'
updateLock = require './lib/update-lock'
{ singleToMulticontainerApp } = require './lib/migration'
{ ENOENT, EISDIR, NotFoundError } = require './lib/errors'
{ ENOENT, EISDIR, NotFoundError, UpdatesLockedError } = require './lib/errors'
DeviceConfig = require './device-config'
ApplicationManager = require './application-manager'
@ -60,7 +60,7 @@ createDeviceStateRouter = (deviceState) ->
.then (response) ->
res.status(202).json(response)
.catch (err) ->
if err instanceof updateLock.UpdatesLockedError
if err instanceof UpdatesLockedError
status = 423
else
status = 500

View File

@ -42,3 +42,5 @@ export class InvalidAppIdError extends TypedError {
super(`Invalid appId: ${appId}`);
}
}
export class UpdatesLockedError extends TypedError {}

View File

@ -1,62 +0,0 @@
Promise = require 'bluebird'
_ = require 'lodash'
TypedError = require 'typed-error'
lockFile = Promise.promisifyAll(require('lockfile'))
Lock = require 'rwlock'
fs = Promise.promisifyAll(require('fs'))
path = require 'path'
constants = require './constants'
{ ENOENT } = require './errors'
baseLockPath = (appId) ->
return path.join('/tmp/balena-supervisor/services', appId.toString())
exports.lockPath = (appId, serviceName) ->
return path.join(baseLockPath(appId), serviceName)
lockFilesOnHost = (appId, serviceName) ->
return _.map [ 'updates.lock', 'resin-updates.lock' ], (fileName) ->
path.join(constants.rootMountPoint, exports.lockPath(appId, serviceName), fileName)
exports.UpdatesLockedError = class UpdatesLockedError extends TypedError
locksTaken = {}
# Try to clean up any existing locks when the program exits
process.on 'exit', ->
for lockName of locksTaken
try
lockFile.unlockSync(lockName)
exports.lock = do ->
_lock = new Lock()
_writeLock = Promise.promisify(_lock.async.writeLock)
return (appId, { force = false } = {}, fn) ->
takeTheLock = ->
Promise.try ->
return if !appId?
dispose = (release) ->
Promise.map _.keys(locksTaken), (lockName) ->
delete locksTaken[lockName]
lockFile.unlockAsync(lockName)
.finally(release)
_writeLock(appId)
.tap (release) ->
theLockDir = path.join(constants.rootMountPoint, baseLockPath(appId))
fs.readdirAsync(theLockDir)
.catchReturn(ENOENT, [])
.mapSeries (serviceName) ->
Promise.mapSeries lockFilesOnHost(appId, serviceName), (tmpLockName) ->
Promise.try ->
lockFile.unlockAsync(tmpLockName) if force == true
.then ->
lockFile.lockAsync(tmpLockName)
.then ->
locksTaken[tmpLockName] = true
.catchReturn(ENOENT, null)
.catch (err) ->
dispose(release)
.throw(new exports.UpdatesLockedError("Updates are locked: #{err.message}"))
.disposer(dispose)
Promise.using takeTheLock(), -> fn()

View File

@ -1,10 +0,0 @@
import TypedError = require('typed-error');
export interface LockCallback {
(appId: number, opts: { force: boolean }, fn: () => void): Promise<void>;
}
export class UpdatesLockedError extends TypedError {}
export function lock(): LockCallback;
export function lockPath(appId: number, serviceName: string): string;

105
src/lib/update-lock.ts Normal file
View File

@ -0,0 +1,105 @@
import * as Bluebird from 'bluebird';
import * as lockFileLib from 'lockfile';
import * as _ from 'lodash';
import { fs } from 'mz';
import * as path from 'path';
import * as Lock from 'rwlock';
import constants = require('./constants');
import { ENOENT, UpdatesLockedError } from './errors';
type asyncLockFile = typeof lockFileLib & {
unlockAsync(path: string): Bluebird<void>;
lockAsync(path: string): Bluebird<void>;
};
const lockFile = Bluebird.promisifyAll(lockFileLib) as asyncLockFile;
export type LockCallback = (
appId: number,
opts: { force: boolean },
fn: () => PromiseLike<void>,
) => Bluebird<void>;
function baseLockPath(appId: number): string {
return path.join('/tmp/balena-supervisor/services', appId.toString());
}
export function lockPath(appId: number, serviceName: string): string {
return path.join(baseLockPath(appId), serviceName);
}
function lockFilesOnHost(appId: number, serviceName: string): string[] {
return ['updates.lock', 'resin-updates.lock'].map(filename =>
path.join(constants.rootMountPoint, lockPath(appId, serviceName), filename),
);
}
const locksTaken: { [lockName: string]: boolean } = {};
// Try to clean up any existing locks when the program exits
process.on('exit', () => {
for (const lockName of _.keys(locksTaken)) {
try {
lockFile.unlockSync(lockName);
} catch (e) {
// Ignore unlocking errors
}
}
});
const locker = new Lock();
const writeLock = Bluebird.promisify(locker.async.writeLock).bind(locker);
function dispose(release: () => void): Bluebird<void> {
return Bluebird.map(_.keys(locksTaken), lockName => {
delete locksTaken[lockName];
return lockFile.unlockAsync(lockName);
})
.finally(release)
.return();
}
export function lock(
appId: number | null,
{ force = false }: { force: boolean },
fn: () => PromiseLike<void>,
): Bluebird<void> {
const takeTheLock = () => {
if (appId == null) {
return;
}
return writeLock(appId)
.tap((release: () => void) => {
const lockDir = path.join(
constants.rootMountPoint,
baseLockPath(appId),
);
return Bluebird.resolve(fs.readdir(lockDir))
.catchReturn(ENOENT, [])
.mapSeries(serviceName => {
return Bluebird.mapSeries(
lockFilesOnHost(appId, serviceName),
tmpLockName => {
return Bluebird.try(() => {
if (force) {
return lockFile.unlockAsync(tmpLockName);
}
})
.then(() => lockFile.lockAsync(tmpLockName))
.then(() => {
locksTaken[tmpLockName] = true;
})
.catchReturn(ENOENT, undefined);
},
).catch(err => {
return dispose(release).throw(
new UpdatesLockedError(`Updates are locked: ${err.message}`),
);
});
});
})
.disposer(dispose);
};
return Bluebird.using(takeTheLock(), fn);
}