mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-25 08:21:07 +00:00
57207c3539
Change-type: patch Signed-off-by: Christina Ying Wang <christina@balena.io>
768 lines
24 KiB
TypeScript
768 lines
24 KiB
TypeScript
import { expect } from 'chai';
|
|
import * as path from 'path';
|
|
import { promises as fs } from 'fs';
|
|
import { testfs } from 'mocha-pod';
|
|
import type { TestFs } from 'mocha-pod';
|
|
import { setTimeout } from 'timers/promises';
|
|
import { watch } from 'chokidar';
|
|
|
|
import * as updateLock from '~/lib/update-lock';
|
|
import { UpdatesLockedError } from '~/lib/errors';
|
|
import * as config from '~/src/config';
|
|
import * as lockfile from '~/lib/lockfile';
|
|
import { pathOnRoot, pathOnState } from '~/lib/host-utils';
|
|
import { mkdirp } from '~/lib/fs-utils';
|
|
import { takeGlobalLockRW } from '~/lib/process-lock';
|
|
|
|
describe('lib/update-lock', () => {
|
|
before(async () => {
|
|
await config.initialized();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await config.set({ lockOverride: false });
|
|
});
|
|
|
|
describe('abortIfHUPInProgress', () => {
|
|
const breadcrumbFiles = [
|
|
'rollback-health-breadcrumb',
|
|
'rollback-altboot-breadcrumb',
|
|
];
|
|
|
|
const breadcrumbsDir = pathOnState();
|
|
|
|
const createBreadcrumb = (breadcrumb: string) =>
|
|
testfs({
|
|
[path.join(breadcrumbsDir, breadcrumb)]: '',
|
|
}).enable();
|
|
|
|
before(async () => {
|
|
// Ensure the directory exists for all tests
|
|
await fs.mkdir(breadcrumbsDir, { recursive: true });
|
|
});
|
|
|
|
it('should throw if any breadcrumbs exist on host', async () => {
|
|
for (const bc of breadcrumbFiles) {
|
|
const testFs = await createBreadcrumb(bc);
|
|
await expect(updateLock.abortIfHUPInProgress({ force: false }))
|
|
.to.eventually.be.rejectedWith('Waiting for Host OS update to finish')
|
|
.and.be.an.instanceOf(UpdatesLockedError);
|
|
await testFs.restore();
|
|
}
|
|
});
|
|
|
|
it('should resolve to false if no breadcrumbs on host', async () => {
|
|
// check that there are no breadcrumbs already on the directory
|
|
expect(await fs.readdir(breadcrumbsDir)).to.have.lengthOf(0);
|
|
await expect(
|
|
updateLock.abortIfHUPInProgress({ force: false }),
|
|
).to.eventually.equal(false);
|
|
});
|
|
|
|
it('should resolve to true if breadcrumbs are on host but force is passed', async () => {
|
|
for (const bc of breadcrumbFiles) {
|
|
const testFs = await createBreadcrumb(bc);
|
|
await expect(
|
|
updateLock.abortIfHUPInProgress({ force: true }),
|
|
).to.eventually.equal(true);
|
|
await testFs.restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Lock/dispose functionality', () => {
|
|
const testAppId = 1234567;
|
|
const testServiceName = 'test';
|
|
|
|
const supportedLockfiles = ['resin-updates.lock', 'updates.lock'];
|
|
|
|
const takeLocks = () =>
|
|
Promise.all(
|
|
supportedLockfiles.map((lf) =>
|
|
lockfile.lock(
|
|
path.join(lockdir(testAppId, testServiceName), lf),
|
|
updateLock.LOCKFILE_UID,
|
|
),
|
|
),
|
|
);
|
|
|
|
const releaseLocks = async () => {
|
|
await Promise.all(
|
|
(await updateLock.getLocksTaken()).map((lock) => lockfile.unlock(lock)),
|
|
);
|
|
|
|
// Remove any other lockfiles created for the testAppId
|
|
await Promise.all(
|
|
supportedLockfiles.map((lf) =>
|
|
lockfile.unlock(path.join(lockdir(testAppId, testServiceName), lf)),
|
|
),
|
|
);
|
|
};
|
|
|
|
const lockdir = (appId: number, serviceName: string): string =>
|
|
pathOnRoot(updateLock.lockPath(appId, serviceName));
|
|
|
|
const expectLocks = async (
|
|
exists: boolean,
|
|
msg?: string,
|
|
appId = testAppId,
|
|
serviceName = testServiceName,
|
|
) =>
|
|
expect(
|
|
fs.readdir(lockdir(appId, serviceName)),
|
|
msg,
|
|
).to.eventually.deep.equal(exists ? supportedLockfiles : []);
|
|
|
|
before(async () => {
|
|
// Ensure the directory is available for all tests
|
|
await fs.mkdir(lockdir(testAppId, testServiceName), {
|
|
recursive: true,
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Cleanup all locks between tests
|
|
await releaseLocks();
|
|
});
|
|
|
|
it('should take the lock, run the function, then dispose of locks', async () => {
|
|
await expectLocks(
|
|
false,
|
|
'locks should not exist before the lock is taken',
|
|
);
|
|
|
|
await expect(
|
|
updateLock.lock(testAppId, { force: false }, () =>
|
|
// At this point the locks should be taken and not removed
|
|
// until this function has been resolved
|
|
expectLocks(true, 'lockfiles should exist while the lock is active'),
|
|
),
|
|
).to.be.fulfilled;
|
|
|
|
await expectLocks(
|
|
false,
|
|
'locks should not exist after the lock is released',
|
|
);
|
|
});
|
|
|
|
it('should throw UpdatesLockedError if lockfiles exists', async () => {
|
|
// Take the locks before testing
|
|
await takeLocks();
|
|
|
|
await expectLocks(true, 'locks should exist before the lock is taken');
|
|
|
|
await updateLock
|
|
.lock(testAppId, { force: false }, () =>
|
|
Promise.reject(
|
|
'the lock function should not invoke the callback if locks are taken',
|
|
),
|
|
)
|
|
.catch((err) => expect(err).to.be.instanceOf(UpdatesLockedError));
|
|
|
|
// Since the lock-taking with `nobody` uid failed, there should be no locks to dispose of
|
|
expect(await updateLock.getLocksTaken()).to.have.length(0);
|
|
|
|
// Restore the locks that were taken at the beginning of the test
|
|
await releaseLocks();
|
|
});
|
|
|
|
it('should dispose of taken locks on any other errors', async () => {
|
|
await expectLocks(false, 'locks should not exist before lock is called');
|
|
await expect(
|
|
updateLock.lock(
|
|
testAppId,
|
|
{ force: false },
|
|
// At this point 2 lockfiles have been written, so this is testing
|
|
// that even if the function rejects, lockfiles will be disposed of
|
|
() =>
|
|
expectLocks(
|
|
true,
|
|
'locks should be owned by the calling function',
|
|
).then(() => Promise.reject('Test error')),
|
|
),
|
|
).to.be.rejectedWith('Test error');
|
|
|
|
await expectLocks(
|
|
false,
|
|
'locks should be removed if an error happens within the lock callback',
|
|
);
|
|
});
|
|
|
|
it('locks all applications before resolving input function', async () => {
|
|
const appIds = [111, 222, 333];
|
|
|
|
// Set up necessary lock directories
|
|
await Promise.all(
|
|
appIds.map((id) =>
|
|
fs.mkdir(lockdir(id, testServiceName), { recursive: true }),
|
|
),
|
|
);
|
|
|
|
await expect(
|
|
updateLock.lock(appIds, { force: false }, () =>
|
|
// At this point the locks should be taken and not removed
|
|
// until this function has been resolved
|
|
// Both `updates.lock` and `resin-updates.lock` should have been taken
|
|
Promise.all(
|
|
appIds.map((appId) =>
|
|
expectLocks(
|
|
true,
|
|
`locks for app(${appId}) should exist`,
|
|
appId,
|
|
testServiceName,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
).to.eventually.be.fulfilled;
|
|
|
|
// Everything that was locked should have been unlocked after function resolves
|
|
await Promise.all(
|
|
appIds.map((appId) =>
|
|
expectLocks(
|
|
false,
|
|
`locks for app(${appId}) should have been released`,
|
|
appId,
|
|
testServiceName,
|
|
),
|
|
),
|
|
).finally(() =>
|
|
// In case the above fails, we need to make sure to cleanup the lockdir
|
|
Promise.all(
|
|
appIds
|
|
.map((appId) =>
|
|
supportedLockfiles.map((lf) =>
|
|
lockfile.unlock(path.join(lockdir(appId, testServiceName), lf)),
|
|
),
|
|
)
|
|
.flat(),
|
|
),
|
|
);
|
|
});
|
|
|
|
it('resolves input function without locking when appId is null', async () => {
|
|
await takeLocks();
|
|
|
|
await expect(
|
|
updateLock.lock(null as any, { force: false }, () => Promise.resolve()),
|
|
).to.be.fulfilled;
|
|
|
|
await expectLocks(
|
|
true,
|
|
'locks should not be touched by an unrelated lock() call',
|
|
);
|
|
|
|
await releaseLocks();
|
|
});
|
|
|
|
it('unlocks lockfile to resolve function if force option specified', async () => {
|
|
await takeLocks();
|
|
|
|
await expect(
|
|
updateLock.lock(testAppId, { force: true }, () =>
|
|
expectLocks(
|
|
true,
|
|
'locks should be deleted and taken again by the lock() call',
|
|
),
|
|
),
|
|
).to.be.fulfilled;
|
|
|
|
await expectLocks(
|
|
false,
|
|
'using force gave lock ownership to the callback, so they should now be deleted',
|
|
);
|
|
});
|
|
|
|
it('unlocks lockfile to resolve function if lockOverride option specified', async () => {
|
|
await takeLocks();
|
|
|
|
// Change the configuration
|
|
await config.set({ lockOverride: true });
|
|
|
|
await expect(
|
|
updateLock.lock(testAppId, { force: false }, () =>
|
|
expectLocks(
|
|
true,
|
|
'locks should be deleted and taken again by the lock() call because of the override',
|
|
),
|
|
),
|
|
).to.be.fulfilled;
|
|
|
|
await expectLocks(
|
|
false,
|
|
'using lockOverride gave lock ownership to the callback, so they should now be deleted',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getLocksTaken', () => {
|
|
const lockdir = pathOnRoot(updateLock.BASE_LOCK_DIR);
|
|
before(async () => {
|
|
await testfs({
|
|
[lockdir]: {},
|
|
}).enable();
|
|
// TODO: enable mocha-pod to work with empty directories
|
|
await fs.mkdir(`${lockdir}/123/main`, { recursive: true });
|
|
await fs.mkdir(`${lockdir}/123/aux`, { recursive: true });
|
|
await fs.mkdir(`${lockdir}/123/invalid`, { recursive: true });
|
|
});
|
|
after(async () => {
|
|
await fs.rm(`${lockdir}/123`, { recursive: true });
|
|
await testfs.restore();
|
|
});
|
|
|
|
it('resolves with all locks taken with the Supervisor lockfile UID', async () => {
|
|
// Set up valid lockfiles including some directories
|
|
await Promise.all(
|
|
['resin-updates.lock', 'updates.lock'].map((lf) => {
|
|
const p = `${lockdir}/123/main/${lf}`;
|
|
return fs
|
|
.mkdir(p)
|
|
.then(() =>
|
|
fs.chown(p, updateLock.LOCKFILE_UID, updateLock.LOCKFILE_UID),
|
|
);
|
|
}),
|
|
);
|
|
await Promise.all([
|
|
lockfile.lock(
|
|
`${lockdir}/123/aux/updates.lock`,
|
|
updateLock.LOCKFILE_UID,
|
|
),
|
|
lockfile.lock(
|
|
`${lockdir}/123/aux/resin-updates.lock`,
|
|
updateLock.LOCKFILE_UID,
|
|
),
|
|
]);
|
|
|
|
// Set up invalid lockfiles with root UID
|
|
await Promise.all(
|
|
['resin-updates.lock', 'updates.lock'].map((lf) =>
|
|
lockfile.lock(`${lockdir}/123/invalid/${lf}`),
|
|
),
|
|
);
|
|
|
|
const locksTaken = await updateLock.getLocksTaken();
|
|
expect(locksTaken).to.have.length(4);
|
|
expect(locksTaken).to.deep.include.members([
|
|
`${lockdir}/123/aux/resin-updates.lock`,
|
|
`${lockdir}/123/aux/updates.lock`,
|
|
`${lockdir}/123/main/resin-updates.lock`,
|
|
`${lockdir}/123/main/updates.lock`,
|
|
]);
|
|
expect(locksTaken).to.not.deep.include.members([
|
|
`${lockdir}/123/invalid/resin-updates.lock`,
|
|
`${lockdir}/123/invalid/updates.lock`,
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('getServicesLockedByAppId', () => {
|
|
const lockdir = pathOnRoot(updateLock.BASE_LOCK_DIR);
|
|
const validDirs = [
|
|
`${lockdir}/123/one`,
|
|
`${lockdir}/123/two`,
|
|
`${lockdir}/123/three`,
|
|
`${lockdir}/456/server`,
|
|
`${lockdir}/456/client`,
|
|
`${lockdir}/789/main`,
|
|
];
|
|
const validPaths = ['resin-updates.lock', 'updates.lock']
|
|
.map((lf) => validDirs.map((d) => path.join(d, lf)))
|
|
.flat();
|
|
const invalidPaths = [
|
|
// No appId
|
|
`${lockdir}/456/updates.lock`,
|
|
// No service
|
|
`${lockdir}/server/updates.lock`,
|
|
// No appId or service
|
|
`${lockdir}/test/updates.lock`,
|
|
// One of (resin-)updates.lock is missing
|
|
`${lockdir}/123/one/resin-updates.lock`,
|
|
`${lockdir}/123/two/updates.lock`,
|
|
];
|
|
let tFs: TestFs.Enabled;
|
|
beforeEach(async () => {
|
|
tFs = await testfs({
|
|
[lockdir]: {},
|
|
}).enable();
|
|
// TODO: mocha-pod should support empty directories
|
|
await Promise.all(
|
|
validPaths
|
|
.concat(invalidPaths)
|
|
.map((p) => fs.mkdir(path.dirname(p), { recursive: true })),
|
|
);
|
|
});
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
validPaths
|
|
.concat(invalidPaths)
|
|
.map((p) => fs.rm(path.dirname(p), { recursive: true })),
|
|
);
|
|
await tFs.restore();
|
|
});
|
|
|
|
it('should return locks taken by appId', async () => {
|
|
// Set up lockfiles
|
|
await Promise.all(
|
|
validPaths.map((p) => lockfile.lock(p, updateLock.LOCKFILE_UID)),
|
|
);
|
|
|
|
const locksTakenMap = await updateLock.getServicesLockedByAppId();
|
|
expect([...locksTakenMap.keys()]).to.deep.include.members([
|
|
123, 456, 789,
|
|
]);
|
|
// Should register as locked if only `updates.lock` is present
|
|
expect(locksTakenMap.getServices(123)).to.deep.include.members([
|
|
'one',
|
|
'two',
|
|
'three',
|
|
]);
|
|
expect(locksTakenMap.getServices(456)).to.deep.include.members([
|
|
'server',
|
|
'client',
|
|
]);
|
|
// Should register as locked if only `resin-updates.lock` is present
|
|
expect(locksTakenMap.getServices(789)).to.deep.include.members(['main']);
|
|
|
|
// Cleanup lockfiles
|
|
await Promise.all(validPaths.map((p) => lockfile.unlock(p)));
|
|
});
|
|
|
|
it('should ignore invalid lockfile locations', async () => {
|
|
// Set up lockfiles
|
|
await Promise.all(
|
|
invalidPaths.map((p) => lockfile.lock(p, updateLock.LOCKFILE_UID)),
|
|
);
|
|
// Take another lock with an invalid UID but with everything else
|
|
// (appId, service, both lockfiles present) correct
|
|
await Promise.all(
|
|
['resin-updates.lock', 'updates.lock'].map((lf) =>
|
|
lockfile.lock(path.join(`${lockdir}/789/main`, lf)),
|
|
),
|
|
);
|
|
expect((await updateLock.getServicesLockedByAppId()).size).to.equal(0);
|
|
|
|
// Cleanup lockfiles
|
|
await Promise.all(invalidPaths.map((p) => lockfile.unlock(p)));
|
|
});
|
|
});
|
|
|
|
describe('composition step actions', () => {
|
|
const lockdir = pathOnRoot(updateLock.BASE_LOCK_DIR);
|
|
const serviceLockPaths = {
|
|
1: [
|
|
`${lockdir}/1/server/updates.lock`,
|
|
`${lockdir}/1/server/resin-updates.lock`,
|
|
`${lockdir}/1/client/updates.lock`,
|
|
`${lockdir}/1/client/resin-updates.lock`,
|
|
],
|
|
2: [
|
|
`${lockdir}/2/main/updates.lock`,
|
|
`${lockdir}/2/main/resin-updates.lock`,
|
|
],
|
|
};
|
|
|
|
describe('takeLock', () => {
|
|
let testFs: TestFs.Enabled;
|
|
|
|
beforeEach(async () => {
|
|
testFs = await testfs(
|
|
{},
|
|
{ cleanup: [path.join(lockdir, '*', '*', '**.lock')] },
|
|
).enable();
|
|
// TODO: Update mocha-pod to work with creating empty directories
|
|
await mkdirp(path.join(lockdir, '1', 'server'));
|
|
await mkdirp(path.join(lockdir, '1', 'client'));
|
|
await mkdirp(path.join(lockdir, '2', 'main'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await testFs.restore();
|
|
await fs.rm(path.join(lockdir, '1'), { recursive: true });
|
|
await fs.rm(path.join(lockdir, '2'), { recursive: true });
|
|
});
|
|
|
|
it('takes locks for a list of services for an appId', async () => {
|
|
// Take locks for appId 1
|
|
await updateLock.takeLock(1, ['server', 'client']);
|
|
// Locks should have been taken
|
|
expect(await updateLock.getLocksTaken()).to.deep.include.members(
|
|
serviceLockPaths[1],
|
|
);
|
|
expect(await updateLock.getLocksTaken()).to.have.length(4);
|
|
expect(
|
|
await fs.readdir(path.join(lockdir, '1', 'server')),
|
|
).to.include.members(['updates.lock', 'resin-updates.lock']);
|
|
expect(
|
|
await fs.readdir(path.join(lockdir, '1', 'client')),
|
|
).to.include.members(['updates.lock', 'resin-updates.lock']);
|
|
// Take locks for appId 2
|
|
await updateLock.takeLock(2, ['main']);
|
|
// Locks should have been taken for appid 1 & 2
|
|
expect(await updateLock.getLocksTaken()).to.deep.include.members([
|
|
...serviceLockPaths[1],
|
|
...serviceLockPaths[2],
|
|
]);
|
|
expect(await updateLock.getLocksTaken()).to.have.length(6);
|
|
expect(
|
|
await fs.readdir(path.join(lockdir, '2', 'main')),
|
|
).to.have.length(2);
|
|
// Clean up the lockfiles
|
|
for (const lockPath of serviceLockPaths[1].concat(
|
|
serviceLockPaths[2],
|
|
)) {
|
|
await lockfile.unlock(lockPath);
|
|
}
|
|
});
|
|
|
|
it('creates lock directory recursively if it does not exist', async () => {
|
|
// Take locks for app with nonexistent service directories
|
|
await updateLock.takeLock(3, ['api']);
|
|
// Locks should have been taken
|
|
expect(await updateLock.getLocksTaken()).to.deep.include(
|
|
path.join(lockdir, '3', 'api', 'updates.lock'),
|
|
path.join(lockdir, '3', 'api', 'resin-updates.lock'),
|
|
);
|
|
// Directories should have been created
|
|
expect(await fs.readdir(path.join(lockdir))).to.deep.include.members([
|
|
'3',
|
|
]);
|
|
expect(
|
|
await fs.readdir(path.join(lockdir, '3')),
|
|
).to.deep.include.members(['api']);
|
|
// Clean up the lockfiles & created directories
|
|
await lockfile.unlock(path.join(lockdir, '3', 'api', 'updates.lock'));
|
|
await lockfile.unlock(
|
|
path.join(lockdir, '3', 'api', 'resin-updates.lock'),
|
|
);
|
|
await fs.rm(path.join(lockdir, '3'), { recursive: true });
|
|
});
|
|
|
|
it('should not take lock for services where Supervisor-taken lock already exists', async () => {
|
|
// Take locks for one service of appId 1
|
|
await lockfile.lock(serviceLockPaths[1][0], updateLock.LOCKFILE_UID);
|
|
await lockfile.lock(serviceLockPaths[1][1], updateLock.LOCKFILE_UID);
|
|
// Sanity check that locks are taken & tracked by Supervisor
|
|
expect(await updateLock.getLocksTaken()).to.deep.include(
|
|
serviceLockPaths[1][0],
|
|
serviceLockPaths[1][1],
|
|
);
|
|
expect(await updateLock.getLocksTaken()).to.have.length(2);
|
|
// Take locks using takeLock, should only lock service which doesn't
|
|
// already have locks
|
|
await expect(
|
|
updateLock.takeLock(1, ['server', 'client']),
|
|
).to.eventually.deep.include.members(['client']);
|
|
// Check that locks are taken
|
|
expect(await updateLock.getLocksTaken()).to.deep.include.members(
|
|
serviceLockPaths[1],
|
|
);
|
|
// Clean up lockfiles
|
|
for (const lockPath of serviceLockPaths[1]) {
|
|
await lockfile.unlock(lockPath);
|
|
}
|
|
});
|
|
|
|
it('should error if service has a non-Supervisor-taken lock', async () => {
|
|
// Simulate a user service taking the lock for services with appId 1
|
|
for (const lockPath of serviceLockPaths[1]) {
|
|
await fs.writeFile(lockPath, '');
|
|
}
|
|
// Take locks using takeLock, should error
|
|
await expect(
|
|
updateLock.takeLock(1, ['server', 'client']),
|
|
).to.eventually.be.rejectedWith(UpdatesLockedError);
|
|
// No Supervisor locks should have been taken
|
|
expect(await updateLock.getLocksTaken()).to.have.length(0);
|
|
// Clean up user-created lockfiles
|
|
for (const lockPath of serviceLockPaths[1]) {
|
|
await fs.rm(lockPath);
|
|
}
|
|
// Take locks using takeLock, should not error
|
|
await expect(
|
|
updateLock.takeLock(1, ['server', 'client']),
|
|
).to.eventually.not.be.rejectedWith(UpdatesLockedError);
|
|
// Check that locks are taken
|
|
expect(await updateLock.getLocksTaken()).to.deep.include.members(
|
|
serviceLockPaths[1],
|
|
);
|
|
expect(await updateLock.getLocksTaken()).to.have.length(4);
|
|
// Clean up lockfiles
|
|
for (const lockPath of serviceLockPaths[1]) {
|
|
await lockfile.unlock(lockPath);
|
|
}
|
|
});
|
|
|
|
it('should take locks if service has non-Supervisor-taken lock and force is true', async () => {
|
|
// Simulate a user service taking the lock for services with appId 1
|
|
for (const lockPath of serviceLockPaths[1]) {
|
|
await fs.writeFile(lockPath, '');
|
|
}
|
|
// Take locks using takeLock & force, should not error
|
|
await updateLock.takeLock(1, ['server', 'client'], true);
|
|
// Check that locks are taken
|
|
expect(await updateLock.getLocksTaken()).to.deep.include(
|
|
serviceLockPaths[1][0],
|
|
serviceLockPaths[1][1],
|
|
);
|
|
// Clean up lockfiles
|
|
for (const lockPath of serviceLockPaths[1]) {
|
|
await lockfile.unlock(lockPath);
|
|
}
|
|
});
|
|
|
|
it('waits to take locks until resource write lock is taken', async () => {
|
|
// Take the write lock for appId 1
|
|
const release = await takeGlobalLockRW(1);
|
|
// Queue takeLock, won't resolve until the write lock is released
|
|
const takeLockPromise = updateLock.takeLock(1, ['server', 'client']);
|
|
// Locks should have not been taken even after waiting
|
|
await setTimeout(500);
|
|
expect(await updateLock.getLocksTaken()).to.have.length(0);
|
|
// Release the write lock
|
|
release();
|
|
// Locks should be taken
|
|
await takeLockPromise;
|
|
// Locks should have been taken
|
|
expect(await updateLock.getLocksTaken()).to.deep.include.members(
|
|
serviceLockPaths[1],
|
|
);
|
|
});
|
|
|
|
it('should release locks when takeLock step errors to return services to unlocked state', async () => {
|
|
const svcs = ['server', 'client'];
|
|
|
|
// Take lock for second service of two services
|
|
await lockfile.lock(`${lockdir}/1/${svcs[1]}/updates.lock`);
|
|
expect(await lockfile.getLocksTaken(lockdir)).to.deep.include.members([
|
|
`${lockdir}/1/${svcs[1]}/updates.lock`,
|
|
]);
|
|
|
|
// Watch for added files, as Supervisor-taken locks should be added
|
|
// then removed within updateLock.takeLock
|
|
const addedFiles: string[] = [];
|
|
const watcher = watch(lockdir).on('add', (p) => addedFiles.push(p));
|
|
|
|
// updateLock.takeLock should error
|
|
await expect(updateLock.takeLock(1, svcs, false)).to.be.rejectedWith(
|
|
UpdatesLockedError,
|
|
);
|
|
|
|
// Service without user lock should have been locked by Supervisor..
|
|
expect(addedFiles).to.deep.include.members([
|
|
`${lockdir}/1/${svcs[0]}/updates.lock`,
|
|
`${lockdir}/1/${svcs[0]}/resin-updates.lock`,
|
|
]);
|
|
|
|
// ..but upon error, Supervisor-taken locks should have been cleaned up
|
|
expect(
|
|
await lockfile.getLocksTaken(lockdir),
|
|
).to.not.deep.include.members([
|
|
`${lockdir}/1/${svcs[0]}/updates.lock`,
|
|
`${lockdir}/1/${svcs[0]}/resin-updates.lock`,
|
|
]);
|
|
|
|
// User lock should be left behind
|
|
expect(await lockfile.getLocksTaken(lockdir)).to.deep.include.members([
|
|
`${lockdir}/1/${svcs[1]}/updates.lock`,
|
|
]);
|
|
|
|
// Clean up watcher
|
|
await watcher.close();
|
|
});
|
|
});
|
|
|
|
describe('releaseLock', () => {
|
|
let testFs: TestFs.Enabled;
|
|
|
|
beforeEach(async () => {
|
|
testFs = await testfs(
|
|
{},
|
|
{ cleanup: [path.join(lockdir, '*', '*', '**.lock')] },
|
|
).enable();
|
|
// TODO: Update mocha-pod to work with creating empty directories
|
|
await mkdirp(`${lockdir}/1/server`);
|
|
await mkdirp(`${lockdir}/1/client`);
|
|
await mkdirp(`${lockdir}/2/main`);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await testFs.restore();
|
|
await fs.rm(`${lockdir}/1`, { recursive: true });
|
|
await fs.rm(`${lockdir}/2`, { recursive: true });
|
|
});
|
|
|
|
it('releases locks for an appId', async () => {
|
|
// Lock services for appId 1
|
|
for (const lockPath of serviceLockPaths[1]) {
|
|
await lockfile.lock(lockPath, updateLock.LOCKFILE_UID);
|
|
}
|
|
// Sanity check that locks are taken & tracked by Supervisor
|
|
expect(await updateLock.getLocksTaken()).to.deep.include.members(
|
|
serviceLockPaths[1],
|
|
);
|
|
// Release locks for appId 1
|
|
await updateLock.releaseLock(1);
|
|
// Locks should have been released
|
|
expect(await updateLock.getLocksTaken()).to.have.length(0);
|
|
// Double check that the lockfiles are removed
|
|
expect(await fs.readdir(`${lockdir}/1/server`)).to.have.length(0);
|
|
expect(await fs.readdir(`${lockdir}/1/client`)).to.have.length(0);
|
|
});
|
|
|
|
it('does not error if there are no locks to release', async () => {
|
|
expect(await updateLock.getLocksTaken()).to.have.length(0);
|
|
// Should not error
|
|
await updateLock.releaseLock(1);
|
|
expect(await updateLock.getLocksTaken()).to.have.length(0);
|
|
});
|
|
|
|
it('ignores locks outside of appId scope', async () => {
|
|
const lockPath = `${lockdir}/2/main/updates.lock`;
|
|
// Lock services outside of appId scope
|
|
await lockfile.lock(lockPath, updateLock.LOCKFILE_UID);
|
|
// Sanity check that locks are taken & tracked by Supervisor
|
|
expect(await updateLock.getLocksTaken()).to.deep.include.members([
|
|
lockPath,
|
|
]);
|
|
// Release locks for appId 1
|
|
await updateLock.releaseLock(1);
|
|
// Locks for appId 2 should not have been released
|
|
expect(await updateLock.getLocksTaken()).to.deep.include.members([
|
|
lockPath,
|
|
]);
|
|
// Double check that the lockfile is still there
|
|
expect(await fs.readdir(`${lockdir}/2/main`)).to.have.length(1);
|
|
// Clean up the lockfile
|
|
await lockfile.unlock(lockPath);
|
|
});
|
|
|
|
it('waits to release locks until resource write lock is taken', async () => {
|
|
// Lock services for appId 1
|
|
for (const lockPath of serviceLockPaths[1]) {
|
|
await lockfile.lock(lockPath, updateLock.LOCKFILE_UID);
|
|
}
|
|
// Sanity check that locks are taken & tracked by Supervisor
|
|
expect(await updateLock.getLocksTaken()).to.deep.include.members(
|
|
serviceLockPaths[1],
|
|
);
|
|
// Take the write lock for appId 1
|
|
const release = await takeGlobalLockRW(1);
|
|
// Queue releaseLock, won't resolve until the write lock is released
|
|
const releaseLockPromise = updateLock.releaseLock(1);
|
|
// Locks should have not been released even after waiting
|
|
await setTimeout(500);
|
|
expect(await updateLock.getLocksTaken()).to.deep.include.members(
|
|
serviceLockPaths[1],
|
|
);
|
|
// Release the write lock
|
|
release();
|
|
// Release locks for appId 1 should resolve
|
|
await releaseLockPromise;
|
|
// Locks should have been released
|
|
expect(await updateLock.getLocksTaken()).to.have.length(0);
|
|
});
|
|
});
|
|
});
|
|
});
|