mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-05-03 17:42:56 +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/event-stream": "^3.3.34",
|
||||||
"@types/express": "^4.11.1",
|
"@types/express": "^4.11.1",
|
||||||
"@types/knex": "^0.14.14",
|
"@types/knex": "^0.14.14",
|
||||||
|
"@types/lockfile": "^1.0.0",
|
||||||
"@types/lodash": "^4.14.109",
|
"@types/lodash": "^4.14.109",
|
||||||
"@types/memoizee": "^0.4.2",
|
"@types/memoizee": "^0.4.2",
|
||||||
"@types/mz": "0.0.32",
|
"@types/mz": "0.0.32",
|
||||||
|
@ -17,7 +17,7 @@ validation = require './lib/validation'
|
|||||||
systemd = require './lib/systemd'
|
systemd = require './lib/systemd'
|
||||||
updateLock = require './lib/update-lock'
|
updateLock = require './lib/update-lock'
|
||||||
{ singleToMulticontainerApp } = require './lib/migration'
|
{ singleToMulticontainerApp } = require './lib/migration'
|
||||||
{ ENOENT, EISDIR, NotFoundError } = require './lib/errors'
|
{ ENOENT, EISDIR, NotFoundError, UpdatesLockedError } = require './lib/errors'
|
||||||
|
|
||||||
DeviceConfig = require './device-config'
|
DeviceConfig = require './device-config'
|
||||||
ApplicationManager = require './application-manager'
|
ApplicationManager = require './application-manager'
|
||||||
@ -60,7 +60,7 @@ createDeviceStateRouter = (deviceState) ->
|
|||||||
.then (response) ->
|
.then (response) ->
|
||||||
res.status(202).json(response)
|
res.status(202).json(response)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
if err instanceof updateLock.UpdatesLockedError
|
if err instanceof UpdatesLockedError
|
||||||
status = 423
|
status = 423
|
||||||
else
|
else
|
||||||
status = 500
|
status = 500
|
||||||
|
@ -42,3 +42,5 @@ export class InvalidAppIdError extends TypedError {
|
|||||||
super(`Invalid appId: ${appId}`);
|
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…
x
Reference in New Issue
Block a user