balena-supervisor/src/compose/update-strategies.ts
Christina Ying Wang 10f294cf8e Add takeLock to state funnel
A takeLock step should be generated before any of the following steps:
* kill
* start
* stop
* updateMetadata
* restart
* handover

ALL services in an app will be locked for any of the above actions,
unless the action is generated through Supervisor API's
`POST /v2/applications/:appId/(start|stop|restart)-service` endpoints,
in which case only the target service will be locked.

A lock will be taken for a service before it starts by creating the
directory in /tmp before the Engine creates it through bind mounts.

Also, the commit simplifies the generation of service kill
steps from network/volume changes or removals.

Signed-off-by: Christina Ying Wang <christina@balena.io>
2024-04-04 14:07:47 -07:00

117 lines
3.0 KiB
TypeScript

import * as imageManager from './images';
import type Service from './service';
import type { CompositionStep } from './composition-steps';
import { generateStep } from './composition-steps';
import type { AppsToLockMap } from './app';
import { InternalInconsistencyError } from '../lib/errors';
import { checkString } from '../lib/validation';
export interface StrategyContext {
current: Service;
target?: Service;
needsDownload: boolean;
dependenciesMetForStart: boolean;
dependenciesMetForKill: boolean;
needsSpecialKill: boolean;
services: Service[];
servicesLocked: boolean;
appsToLock: AppsToLockMap;
}
function generateLockThenKillStep(
current: Service,
currentServices: Service[],
servicesLocked: boolean,
appsToLock: AppsToLockMap,
): CompositionStep[] {
if (!servicesLocked) {
currentServices.forEach((svc) =>
appsToLock[svc.appId].add(svc.serviceName),
);
return [];
}
return [generateStep('kill', { current })];
}
export function getStepsFromStrategy(
strategy: string,
context: StrategyContext,
): CompositionStep[] {
switch (strategy) {
case 'download-then-kill':
if (context.needsDownload && context.target) {
return [
generateStep('fetch', {
image: imageManager.imageFromService(context.target),
serviceName: context.target.serviceName,
}),
];
} else if (context.dependenciesMetForKill) {
// We only kill when dependencies are already met, so that we minimize downtime
return generateLockThenKillStep(
context.current,
context.services,
context.servicesLocked,
context.appsToLock,
);
} else {
return [generateStep('noop', {})];
}
case 'kill-then-download':
case 'delete-then-download':
return generateLockThenKillStep(
context.current,
context.services,
context.servicesLocked,
context.appsToLock,
);
case 'hand-over':
if (context.needsDownload && context.target) {
return [
generateStep('fetch', {
image: imageManager.imageFromService(context.target),
serviceName: context.target.serviceName,
}),
];
} else if (context.needsSpecialKill && context.dependenciesMetForKill) {
return generateLockThenKillStep(
context.current,
context.services,
context.servicesLocked,
context.appsToLock,
);
} else if (context.dependenciesMetForStart && context.target) {
return [
generateStep('handover', {
current: context.current,
target: context.target,
}),
];
} else {
return [generateStep('noop', {})];
}
default:
throw new InternalInconsistencyError(
`Invalid update strategy: ${strategy}`,
);
}
}
export function getStrategyFromService(svc: Service): string {
let strategy =
checkString(svc.config.labels['io.balena.update.strategy']) || '';
const validStrategies = [
'download-then-kill',
'kill-then-download',
'delete-then-download',
'hand-over',
];
if (!validStrategies.includes(strategy)) {
strategy = 'download-then-kill';
}
return strategy;
}