Simplify lock interface to prep for adding takeLock to state funnel

This commit changes a few things:

* Pass `force` to `takeLock` step directly. This allows us to remove
the `lockFn` used by app manager's action executors, setting takeLock
as the main interface to interact with the update lock module. Note
that this commit by itself will not pass tests, as no update locking
occurs where it once did. This will be amended in the next commit.

* Remove locking functions from doRestart & doPurge, as this is
the only area where skipLock is required.

* Remove `skipLock` interface, as it's redundant with the functionality
of `force`. The only time `skipLock` is true is in doRestart/doPurge,
as those API methods are already run within a lock function. We removed
the lock function which removes the need for skipLock, and in the next
commit we'll add locking as a composition step to replace the
functionality removed here.

* Remove some methods not in use, such as app manager's `stopAll`.

Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
Christina Ying Wang
2024-03-05 23:44:31 -08:00
parent 2f728ee43e
commit cf8d8cedd7
9 changed files with 480 additions and 222 deletions

View File

@ -4,6 +4,7 @@ import { stub } from 'sinon';
import Docker from 'dockerode';
import request from 'supertest';
import { setTimeout } from 'timers/promises';
import { testfs } from 'mocha-pod';
import * as deviceState from '~/src/device-state';
import * as config from '~/src/config';
@ -11,10 +12,12 @@ import * as hostConfig from '~/src/host-config';
import * as deviceApi from '~/src/device-api';
import * as actions from '~/src/device-api/actions';
import * as TargetState from '~/src/device-state/target-state';
import * as updateLock from '~/lib/update-lock';
import { pathOnRoot } from '~/lib/host-utils';
import { exec } from '~/lib/fs-utils';
import * as lockfile from '~/lib/lockfile';
import { cleanupDocker } from '~/test-lib/docker-helper';
import { exec } from '~/src/lib/fs-utils';
export async function dbusSend(
dest: string,
path: string,
@ -79,6 +82,7 @@ describe('manages application lifecycle', () => {
const BALENA_SUPERVISOR_ADDRESS =
process.env.BALENA_SUPERVISOR_ADDRESS || 'http://balena-supervisor:48484';
const APP_ID = 1;
const lockdir = pathOnRoot(updateLock.BASE_LOCK_DIR);
const docker = new Docker();
const getSupervisorTarget = async () =>
@ -218,6 +222,11 @@ describe('manages application lifecycle', () => {
ctns.every(({ State }) => !startedAt.includes(State.StartedAt));
};
const mockFs = testfs(
{ [`${lockdir}/${APP_ID}`]: {} },
{ cleanup: [`${lockdir}/${APP_ID}/**/*.lock`] },
);
before(async () => {
// Images are ignored in local mode so we need to pull the base image
await docker.pull(BASE_IMAGE);
@ -251,10 +260,16 @@ describe('manages application lifecycle', () => {
});
beforeEach(async () => {
await mockFs.enable();
// Create a single-container application in local mode
await setSupervisorTarget(targetState);
});
afterEach(async () => {
await mockFs.restore();
});
// Make sure the app is running and correct before testing more assertions
it('should setup a single container app (sanity check)', async () => {
containers = await waitForSetup(targetState);
@ -292,6 +307,69 @@ describe('manages application lifecycle', () => {
);
});
it('should not restart an application when user locks are present', async () => {
containers = await waitForSetup(targetState);
// Create a lock
await lockfile.lock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
await request(BALENA_SUPERVISOR_ADDRESS)
.post(`/v1/restart`)
.set('Content-Type', 'application/json')
.send(JSON.stringify({ appId: APP_ID }))
.expect(423);
// Containers should not have been restarted
const containersAfterRestart = await waitForSetup(targetState);
expect(
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
// Remove the lock
await lockfile.unlock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
});
it('should restart an application when user locks are present if force is specified', async () => {
containers = await waitForSetup(targetState);
const isRestartSuccessful = startTimesChanged(
containers.map((ctn) => ctn.State.StartedAt),
);
// Create a lock
await lockfile.lock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
await request(BALENA_SUPERVISOR_ADDRESS)
.post(`/v1/restart`)
.set('Content-Type', 'application/json')
.send(JSON.stringify({ appId: APP_ID, force: true }));
const restartedContainers = await waitForSetup(
targetState,
isRestartSuccessful,
);
// Technically the wait function above should already verify that the two
// containers have been restarted, but verify explcitly with an assertion
expect(isRestartSuccessful(restartedContainers)).to.be.true;
// Containers should have different Ids since they're recreated
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
containers.map((ctn) => ctn.Id),
);
// Wait briefly for state to settle which includes releasing locks
await setTimeout(1000);
// User lock should be overridden
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
});
it('should restart service by removing and recreating corresponding container', async () => {
containers = await waitForSetup(targetState);
const isRestartSuccessful = startTimesChanged(
@ -321,6 +399,73 @@ describe('manages application lifecycle', () => {
);
});
// Since restart-service follows the same code paths as start|stop-service,
// these lock test cases should be sufficient to cover all three service actions.
it('should not restart service when user locks are present', async () => {
containers = await waitForSetup(targetState);
// Create a lock
await lockfile.lock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
await request(BALENA_SUPERVISOR_ADDRESS)
.post('/v2/applications/1/restart-service')
.set('Content-Type', 'application/json')
.send(JSON.stringify({ serviceName: serviceNames[0] }))
.expect(423);
// Containers should not have been restarted
const containersAfterRestart = await waitForSetup(targetState);
expect(
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
// Remove the lock
await lockfile.unlock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
});
it('should restart service when user locks are present if force is specified', async () => {
containers = await waitForSetup(targetState);
const isRestartSuccessful = startTimesChanged(
containers
.filter((ctn) => ctn.Name.includes(serviceNames[0]))
.map((ctn) => ctn.State.StartedAt),
);
// Create a lock
await lockfile.lock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
await request(BALENA_SUPERVISOR_ADDRESS)
.post('/v2/applications/1/restart-service')
.set('Content-Type', 'application/json')
.send(JSON.stringify({ serviceName: serviceNames[0], force: true }));
const restartedContainers = await waitForSetup(
targetState,
isRestartSuccessful,
);
// Technically the wait function above should already verify that the two
// containers have been restarted, but verify explcitly with an assertion
expect(isRestartSuccessful(restartedContainers)).to.be.true;
// Containers should have different Ids since they're recreated
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
containers.map((ctn) => ctn.Id),
);
// Wait briefly for state to settle which includes releasing locks
await setTimeout(1000);
// User lock should be overridden
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
});
it('should stop a running service', async () => {
containers = await waitForSetup(targetState);
@ -520,10 +665,15 @@ describe('manages application lifecycle', () => {
});
beforeEach(async () => {
await mockFs.enable();
// Create a multi-container application in local mode
await setSupervisorTarget(targetState);
});
afterEach(async () => {
await mockFs.restore();
});
// Make sure the app is running and correct before testing more assertions
it('should setup a multi-container app (sanity check)', async () => {
containers = await waitForSetup(targetState);
@ -560,6 +710,69 @@ describe('manages application lifecycle', () => {
);
});
it('should not restart an application when user locks are present', async () => {
containers = await waitForSetup(targetState);
// Create a lock
await lockfile.lock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
await request(BALENA_SUPERVISOR_ADDRESS)
.post(`/v1/restart`)
.set('Content-Type', 'application/json')
.send(JSON.stringify({ appId: APP_ID }))
.expect(423);
// Containers should not have been restarted
const containersAfterRestart = await waitForSetup(targetState);
expect(
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
// Remove the lock
await lockfile.unlock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
});
it('should restart an application when user locks are present if force is specified', async () => {
containers = await waitForSetup(targetState);
const isRestartSuccessful = startTimesChanged(
containers.map((ctn) => ctn.State.StartedAt),
);
// Create a lock
await lockfile.lock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
await request(BALENA_SUPERVISOR_ADDRESS)
.post(`/v1/restart`)
.set('Content-Type', 'application/json')
.send(JSON.stringify({ appId: APP_ID, force: true }));
const restartedContainers = await waitForSetup(
targetState,
isRestartSuccessful,
);
// Technically the wait function above should already verify that the two
// containers have been restarted, but verify explcitly with an assertion
expect(isRestartSuccessful(restartedContainers)).to.be.true;
// Containers should have different Ids since they're recreated
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
containers.map((ctn) => ctn.Id),
);
// Wait briefly for state to settle which includes releasing locks
await setTimeout(500);
// User lock should be overridden
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
});
it('should restart service by removing and recreating corresponding container', async () => {
containers = await waitForSetup(targetState);
const serviceName = serviceNames[0];
@ -601,6 +814,73 @@ describe('manages application lifecycle', () => {
expect(sharedIds.length).to.equal(1);
});
// Since restart-service follows the same code paths as start|stop-service,
// these lock test cases should be sufficient to cover all three service actions.
it('should not restart service when user locks are present', async () => {
containers = await waitForSetup(targetState);
// Create a lock
await lockfile.lock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
await request(BALENA_SUPERVISOR_ADDRESS)
.post('/v2/applications/1/restart-service')
.set('Content-Type', 'application/json')
.send(JSON.stringify({ serviceName: serviceNames[0] }))
.expect(423);
// Containers should not have been restarted
const containersAfterRestart = await waitForSetup(targetState);
expect(
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
// Remove the lock
await lockfile.unlock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
});
it('should restart service when user locks are present if force is specified', async () => {
containers = await waitForSetup(targetState);
const isRestartSuccessful = startTimesChanged(
containers
.filter((ctn) => ctn.Name.includes(serviceNames[0]))
.map((ctn) => ctn.State.StartedAt),
);
// Create a lock
await lockfile.lock(
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
);
await request(BALENA_SUPERVISOR_ADDRESS)
.post('/v2/applications/1/restart-service')
.set('Content-Type', 'application/json')
.send(JSON.stringify({ serviceName: serviceNames[0], force: true }));
const restartedContainers = await waitForSetup(
targetState,
isRestartSuccessful,
);
// Technically the wait function above should already verify that the two
// containers have been restarted, but verify explcitly with an assertion
expect(isRestartSuccessful(restartedContainers)).to.be.true;
// Containers should have different Ids since they're recreated
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
containers.map((ctn) => ctn.Id),
);
// Wait briefly for state to settle which includes releasing locks
await setTimeout(500);
// User lock should be overridden
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
});
it('should stop a running service', async () => {
containers = await waitForSetup(targetState);