Merge pull request #1824 from balena-os/synchronize-kill

Wait for images to be ready before moving between releases
This commit is contained in:
bulldozer-balena[bot]
2021-11-11 18:01:03 +00:00
committed by GitHub
2 changed files with 172 additions and 27 deletions

View File

@ -488,7 +488,9 @@ export class App {
// Either this is a new service, or the current one has already been killed // Either this is a new service, or the current one has already been killed
return this.generateFetchOrStartStep( return this.generateFetchOrStartStep(
target!, target!,
context.targetApp,
needsDownload, needsDownload,
context.availableImages,
context.networkPairs, context.networkPairs,
context.volumePairs, context.volumePairs,
context.servicePairs, context.servicePairs,
@ -520,12 +522,13 @@ export class App {
const dependenciesMetForStart = this.dependenciesMetForServiceStart( const dependenciesMetForStart = this.dependenciesMetForServiceStart(
target, target,
context.targetApp,
context.availableImages,
context.networkPairs, context.networkPairs,
context.volumePairs, context.volumePairs,
context.servicePairs, context.servicePairs,
); );
const dependenciesMetForKill = this.dependenciesMetForServiceKill( const dependenciesMetForKill = this.dependenciesMetForServiceKill(
target,
context.targetApp, context.targetApp,
context.availableImages, context.availableImages,
context.localMode, context.localMode,
@ -591,7 +594,9 @@ export class App {
private generateFetchOrStartStep( private generateFetchOrStartStep(
target: Service, target: Service,
targetApp: App,
needsDownload: boolean, needsDownload: boolean,
availableImages: Image[],
networkPairs: Array<ChangingPair<Network>>, networkPairs: Array<ChangingPair<Network>>,
volumePairs: Array<ChangingPair<Volume>>, volumePairs: Array<ChangingPair<Volume>>,
servicePairs: Array<ChangingPair<Service>>, servicePairs: Array<ChangingPair<Service>>,
@ -605,6 +610,8 @@ export class App {
} else if ( } else if (
this.dependenciesMetForServiceStart( this.dependenciesMetForServiceStart(
target, target,
targetApp,
availableImages,
networkPairs, networkPairs,
volumePairs, volumePairs,
servicePairs, servicePairs,
@ -618,6 +625,8 @@ export class App {
// TODO: support networks instead of only network mode // TODO: support networks instead of only network mode
private dependenciesMetForServiceStart( private dependenciesMetForServiceStart(
target: Service, target: Service,
targetApp: App,
availableImages: Image[],
networkPairs: Array<ChangingPair<Network>>, networkPairs: Array<ChangingPair<Network>>,
volumePairs: Array<ChangingPair<Volume>>, volumePairs: Array<ChangingPair<Volume>>,
servicePairs: Array<ChangingPair<Service>>, servicePairs: Array<ChangingPair<Service>>,
@ -626,7 +635,7 @@ export class App {
// different to a dependency which is in the servicePairs below, as these // different to a dependency which is in the servicePairs below, as these
// are services which are changing). We could have a dependency which is // are services which are changing). We could have a dependency which is
// starting up, but is not yet running. // starting up, but is not yet running.
const depInstallingButNotRunning = _.some(this.services, (svc) => { const depInstallingButNotRunning = _.some(targetApp.services, (svc) => {
if (target.dependsOn?.includes(svc.serviceName!)) { if (target.dependsOn?.includes(svc.serviceName!)) {
if (!svc.config.running) { if (!svc.config.running) {
return true; return true;
@ -664,16 +673,15 @@ export class App {
return false; return false;
} }
// everything is ready for the service to start... // do not start until all images have been downloaded
return true; return this.targetImagesReady(targetApp, availableImages);
} }
// Unless the update strategy requires an early kill (i.e kill-then-download, // Unless the update strategy requires an early kill (i.e kill-then-download,
// delete-then-download), we only want to kill a service once the images for the // delete-then-download), we only want to kill a service once the images for the
// services it depends on have been downloaded, so as to minimize downtime (but not // services it depends on have been downloaded, so as to minimize downtime (but not
// block the killing too much, potentially causing a daedlock) // block the killing too much, potentially causing a deadlock)
private dependenciesMetForServiceKill( private dependenciesMetForServiceKill(
target: Service,
targetApp: App, targetApp: App,
availableImages: Image[], availableImages: Image[],
localMode: boolean, localMode: boolean,
@ -685,26 +693,18 @@ export class App {
return true; return true;
} }
if (target.dependsOn != null) { // Don't kill any services before all images have been downloaded
for (const dependency of target.dependsOn) { return this.targetImagesReady(targetApp, availableImages);
const dependencyService = _.find(targetApp.services, { }
serviceName: dependency,
}); private targetImagesReady(targetApp: App, availableImages: Image[]) {
if ( return targetApp.services.every((service) =>
!_.some( availableImages.some(
availableImages, (image) =>
(image) => image.dockerImageId === service.config.image ||
image.dockerImageId === dependencyService?.config.image || imageManager.isSameImage(image, { name: service.imageName! }),
imageManager.isSameImage(image, { ),
name: dependencyService?.imageName!, );
}),
)
) {
return false;
}
}
}
return true;
} }
public static async fromTargetState( public static async fromTargetState(

View File

@ -1147,7 +1147,7 @@ describe('compose/app', () => {
.that.deep.includes({ serviceName: 'test' }); .that.deep.includes({ serviceName: 'test' });
}); });
it('should not create a service when a network it depends on is not ready', async () => { it('should not start a service when a network it depends on is not ready', async () => {
const current = createApp({ networks: [defaultNetwork] }); const current = createApp({ networks: [defaultNetwork] });
const target = createApp({ const target = createApp({
services: [await createService({ networks: ['test'], appId: 1 })], services: [await createService({ networks: ['test'], appId: 1 })],
@ -1199,6 +1199,151 @@ describe('compose/app', () => {
const steps = current.nextStepsForAppUpdate(defaultContext, target); const steps = current.nextStepsForAppUpdate(defaultContext, target);
expectSteps('kill', steps, 2); expectSteps('kill', steps, 2);
}); });
it('should not infer a kill step with the default strategy before all target images have been downloaded', async () => {
const contextWithImages = {
...defaultContext,
...{
downloading: ['other-image-2'], // One of the images is being downloaded
availableImages: [
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
createImage({
appId: 1,
name: 'other-image',
serviceName: 'other',
}),
createImage({
appId: 1,
name: 'main-image-2',
serviceName: 'main',
}),
],
},
};
const current = createApp({
services: [
await createService({
image: 'main-image',
appId: 1,
serviceName: 'main',
commit: 'old-release',
}),
await createService({
image: 'other-image',
appId: 1,
serviceName: 'other',
commit: 'old-release',
}),
],
networks: [defaultNetwork],
});
const target = createApp({
services: [
await createService({
image: 'main-image-2',
appId: 1,
serviceName: 'main',
commit: 'new-release',
}),
await createService({
image: 'other-image-2',
appId: 1,
serviceName: 'other',
commit: 'new-release',
}),
],
networks: [defaultNetwork],
isTarget: true,
});
// No kill steps should be generated
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
expectNoStep('kill', steps);
});
it('should not infer a start step before all target images have been downloaded', async () => {
const contextWithImages = {
...defaultContext,
...{
downloading: ['other-image'], // One of the images is being downloaded
availableImages: [
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
],
},
};
const current = createApp({
services: [],
networks: [defaultNetwork],
});
const target = createApp({
services: [
await createService({
image: 'main-image',
appId: 1,
serviceName: 'main',
commit: 'new-release',
}),
await createService({
image: 'other-image',
appId: 1,
serviceName: 'other',
commit: 'new-release',
}),
],
networks: [defaultNetwork],
isTarget: true,
});
// No kill steps should be generated
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
expectNoStep('start', steps);
});
it('should infer a start step only when target images have been downloaded', async () => {
const contextWithImages = {
...defaultContext,
...{
downloading: [], // One of the images is being downloaded
availableImages: [
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
createImage({
appId: 1,
name: 'other-image',
serviceName: 'other',
}),
],
},
};
const current = createApp({
services: [],
networks: [defaultNetwork],
});
const target = createApp({
services: [
await createService({
image: 'main-image',
appId: 1,
serviceName: 'main',
commit: 'new-release',
}),
await createService({
image: 'other-image',
appId: 1,
serviceName: 'other',
commit: 'new-release',
}),
],
networks: [defaultNetwork],
isTarget: true,
});
// No kill steps should be generated
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
expectSteps('start', steps, 2);
});
}); });
describe('image state behavior', () => { describe('image state behavior', () => {