mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-23 15:32:24 +00:00
refactor: Convert update-lock module to typescript
Change-type: patch Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
parent
ec37db597d
commit
b977b30dfe
@ -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",
|
||||
|
@ -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
|
||||
|
@ -42,3 +42,5 @@ export class InvalidAppIdError extends TypedError {
|
||||
super(`Invalid appId: ${appId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdatesLockedError extends TypedError {}
|
||||
|
@ -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()
|
10
src/lib/update-lock.d.ts
vendored
10
src/lib/update-lock.d.ts
vendored
@ -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
105
src/lib/update-lock.ts
Normal 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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user