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

@ -45,6 +45,7 @@ export interface UpdateState {
containerIds: Dictionary<string>;
downloading: string[];
locksTaken: LocksTakenMap;
force: boolean;
}
interface ChangingPair<T> {
@ -111,8 +112,15 @@ export class App {
// Check to see if we need to polyfill in some "new" data for legacy services
this.migrateLegacy(target);
// Check for changes in the volumes. We don't remove any volumes until we remove an
// entire app
let steps: CompositionStep[] = [];
// Any services which have died get a remove step
for (const service of this.services) {
if (service.status === 'Dead') {
steps.push(generateStep('remove', { current: service }));
}
}
const volumeChanges = this.compareComponents(
this.volumes,
target.volumes,
@ -124,15 +132,6 @@ export class App {
true,
);
let steps: CompositionStep[] = [];
// Any services which have died get a remove step
for (const service of this.services) {
if (service.status === 'Dead') {
steps.push(generateStep('remove', { current: service }));
}
}
const { removePairs, installPairs, updatePairs } = this.compareServices(
this.services,
target.services,
@ -571,7 +570,12 @@ export class App {
current.isEqualConfig(target, context.containerIds)
) {
// Update service metadata or start/stop a service
return this.generateContainerStep(current, target, context.locksTaken);
return this.generateContainerStep(
current,
target,
context.locksTaken,
context.force,
);
}
let strategy: string;
@ -664,6 +668,7 @@ export class App {
current: Service,
target: Service,
locksTaken: LocksTakenMap,
force: boolean,
) {
// Update container metadata if service release has changed
if (current.commit !== target.commit) {
@ -679,6 +684,7 @@ export class App {
return generateStep('takeLock', {
appId: target.appId,
services: [target.serviceName],
force,
});
} else if (target.config.running !== current.config.running) {
if (target.config.running) {

View File

@ -17,7 +17,7 @@ import {
ContractViolationError,
InternalInconsistencyError,
} from '../lib/errors';
import { getServicesLockedByAppId, lock } from '../lib/update-lock';
import { getServicesLockedByAppId } from '../lib/update-lock';
import { checkTruthy } from '../lib/validation';
import App from './app';
@ -64,7 +64,6 @@ export function resetTimeSpentFetching(value: number = 0) {
}
const actionExecutors = getExecutors({
lockFn: lock,
callbacks: {
fetchStart: () => {
fetchesInProgress += 1;
@ -120,6 +119,7 @@ export async function getRequiredSteps(
targetApps: InstancedAppState,
keepImages?: boolean,
keepVolumes?: boolean,
force: boolean = false,
): Promise<CompositionStep[]> {
// get some required data
const [downloading, availableImages, { localMode, delta }] =
@ -146,6 +146,7 @@ export async function getRequiredSteps(
// Volumes are not removed when stopping an app when going to local mode
keepVolumes,
delta,
force,
downloading,
availableImages,
containerIdsByAppId,
@ -161,6 +162,7 @@ export async function inferNextSteps(
keepImages = false,
keepVolumes = false,
delta = true,
force = false,
downloading = [] as UpdateState['downloading'],
availableImages = [] as UpdateState['availableImages'],
containerIdsByAppId = {} as {
@ -219,6 +221,7 @@ export async function inferNextSteps(
containerIds: containerIdsByAppId[id],
downloading,
locksTaken,
force,
},
targetApps[id],
),
@ -233,6 +236,7 @@ export async function inferNextSteps(
downloading,
containerIds: containerIdsByAppId[id],
locksTaken,
force,
}),
);
}
@ -257,6 +261,7 @@ export async function inferNextSteps(
containerIds: containerIdsByAppId[id] ?? {},
downloading,
locksTaken,
force,
},
targetApps[id],
),
@ -301,17 +306,6 @@ export async function inferNextSteps(
return steps;
}
export async function stopAll({ force = false, skipLock = false } = {}) {
const services = await serviceManager.getAll();
await Promise.all(
services.map(async (s) => {
return lock(s.appId, { force, skipLock }, async () => {
await serviceManager.kill(s, { removeContainer: false, wait: true });
});
}),
);
}
// The following two function may look pretty odd, but after the move to uuids,
// there's a chance that the current running apps don't have a uuid set. We
// still need to be able to work on these and perform various state changes. To
@ -501,7 +495,7 @@ function killServicesUsingApi(current: InstancedAppState): CompositionStep[] {
// intermediate targets to perform changes
export async function executeStep(
step: CompositionStep,
{ force = false, skipLock = false } = {},
{ force = false } = {},
): Promise<void> {
if (!validActions.includes(step.action)) {
return Promise.reject(
@ -515,7 +509,6 @@ export async function executeStep(
await actionExecutors[step.action]({
...step,
force,
skipLock,
} as any);
}

View File

@ -1,5 +1,3 @@
import _ from 'lodash';
import * as config from '../config';
import type { Image } from './images';
import * as images from './images';
@ -13,33 +11,22 @@ import * as commitStore from './commit';
import * as updateLock from '../lib/update-lock';
import type { DeviceLegacyReport } from '../types/state';
interface BaseCompositionStepArgs {
force?: boolean;
skipLock?: boolean;
}
// FIXME: Most of the steps take the
// BaseCompositionStepArgs, but some also take an options
// structure which includes some of the same fields. It
// would be nice to remove the need for this
interface CompositionStepArgs {
stop: {
current: Service;
options?: {
skipLock?: boolean;
wait?: boolean;
};
} & BaseCompositionStepArgs;
};
kill: {
current: Service;
options?: {
skipLock?: boolean;
wait?: boolean;
};
} & BaseCompositionStepArgs;
};
remove: {
current: Service;
} & BaseCompositionStepArgs;
};
updateMetadata: {
current: Service;
target: Service;
@ -47,13 +34,10 @@ interface CompositionStepArgs {
restart: {
current: Service;
target: Service;
options?: {
skipLock?: boolean;
};
} & BaseCompositionStepArgs;
};
start: {
target: Service;
} & BaseCompositionStepArgs;
};
updateCommit: {
target: string;
appId: number;
@ -62,10 +46,9 @@ interface CompositionStepArgs {
current: Service;
target: Service;
options?: {
skipLock?: boolean;
timeout?: number;
};
} & BaseCompositionStepArgs;
};
fetch: {
image: Image;
serviceName: string;
@ -94,6 +77,7 @@ interface CompositionStepArgs {
takeLock: {
appId: number;
services: string[];
force: boolean;
};
releaseLock: {
appId: number;
@ -119,13 +103,6 @@ export function generateStep<T extends CompositionStepAction>(
type Executors<T extends CompositionStepAction> = {
[key in T]: (step: CompositionStepT<key>) => Promise<unknown>;
};
type LockingFn = (
// TODO: Once the entire codebase is typescript, change
// this to number
app: number | number[] | null,
args: BaseCompositionStepArgs,
fn: () => Promise<unknown>,
) => Promise<unknown>;
interface CompositionCallbacks {
// TODO: Once the entire codebase is typescript, change
@ -137,38 +114,20 @@ interface CompositionCallbacks {
bestDeltaSource: (image: Image, available: Image[]) => string | null;
}
export function getExecutors(app: {
lockFn: LockingFn;
callbacks: CompositionCallbacks;
}) {
export function getExecutors(app: { callbacks: CompositionCallbacks }) {
const executors: Executors<CompositionStepAction> = {
stop: (step) => {
return app.lockFn(
step.current.appId,
{
force: step.force,
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
},
async () => {
const wait = _.get(step, ['options', 'wait'], false);
await serviceManager.kill(step.current, {
removeContainer: false,
wait,
});
},
);
stop: async (step) => {
// Should always be preceded by a takeLock step,
// so the call is executed assuming that the lock is taken.
await serviceManager.kill(step.current, {
removeContainer: false,
wait: step.options?.wait || false,
});
},
kill: (step) => {
return app.lockFn(
step.current.appId,
{
force: step.force,
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
},
async () => {
await serviceManager.kill(step.current);
},
);
kill: async (step) => {
// Should always be preceded by a takeLock step,
// so the call is executed assuming that the lock is taken.
await serviceManager.kill(step.current);
},
remove: async (step) => {
// Only called for dead containers, so no need to
@ -180,18 +139,11 @@ export function getExecutors(app: {
// so the call is executed assuming that the lock is taken.
await serviceManager.updateMetadata(step.current, step.target);
},
restart: (step) => {
return app.lockFn(
step.current.appId,
{
force: step.force,
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
},
async () => {
await serviceManager.kill(step.current, { wait: true });
await serviceManager.start(step.target);
},
);
restart: async (step) => {
// Should always be preceded by a takeLock step,
// so the call is executed assuming that the lock is taken.
await serviceManager.kill(step.current, { wait: true });
await serviceManager.start(step.target);
},
start: async (step) => {
await serviceManager.start(step.target);
@ -199,17 +151,10 @@ export function getExecutors(app: {
updateCommit: async (step) => {
await commitStore.upsertCommitForApp(step.appId, step.target);
},
handover: (step) => {
return app.lockFn(
step.current.appId,
{
force: step.force,
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
},
async () => {
await serviceManager.handover(step.current, step.target);
},
);
handover: async (step) => {
// Should always be preceded by a takeLock step,
// so the call is executed assuming that the lock is taken.
await serviceManager.handover(step.current, step.target);
},
fetch: async (step) => {
const startTime = process.hrtime();
@ -271,7 +216,7 @@ export function getExecutors(app: {
/* async noop */
},
takeLock: async (step) => {
await updateLock.takeLock(step.appId, step.services);
await updateLock.takeLock(step.appId, step.services, step.force);
},
releaseLock: async (step) => {
await updateLock.releaseLock(step.appId);