From 12b67742c8091280c2e74e97a7c7392021db542f Mon Sep 17 00:00:00 2001 From: Christina Wang <christina@balena.io> Date: Tue, 2 Aug 2022 14:34:25 -0700 Subject: [PATCH] Wait for Stopping services to stop before target apply success This mitigates an edge case bug introduced in v13.1.3 where services that are slow to exit may get stuck in a state of Downloaded if a service var is changed then reverted rapidly. More detailed description in linked issue. Change-type: patch Closes: #1991 Signed-off-by: Christina Wang <christina@balena.io> --- src/compose/app.ts | 15 ++++++++++++++- test/src/compose/app.spec.ts | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/compose/app.ts b/src/compose/app.ts index 3cae07fe..c5c81f69 100644 --- a/src/compose/app.ts +++ b/src/compose/app.ts @@ -360,6 +360,18 @@ export class App { ); }; + /** + * Checks if Supervisor should keep the state loop alive while waiting on a service to stop + * @param serviceCurrent + * @param serviceTarget + */ + const shouldWaitForStop = (serviceCurrent: Service) => { + return ( + serviceCurrent.config.running === true && + serviceCurrent.status === 'Stopping' + ); + }; + /** * Filter all the services which should be updated due to run state change, or config mismatch. */ @@ -372,7 +384,8 @@ export class App { ({ current: c, target: t }) => !isEqualExceptForRunningState(c, t) || shouldBeStarted(c, t) || - shouldBeStopped(c, t), + shouldBeStopped(c, t) || + shouldWaitForStop(c), ); return { diff --git a/test/src/compose/app.spec.ts b/test/src/compose/app.spec.ts index 3a9f686f..d6ce0cbd 100644 --- a/test/src/compose/app.spec.ts +++ b/test/src/compose/app.spec.ts @@ -657,6 +657,23 @@ describe('compose/app', () => { expectNoStep('kill', steps); }); + it('should emit a noop while waiting on a stopping service', async () => { + const current = createApp({ + services: [ + await createService( + { serviceName: 'main', running: true }, + { state: { status: 'Stopping' } }, + ), + ], + }); + const target = createApp({ + services: [await createService({ serviceName: 'main', running: true })], + }); + + const steps = current.nextStepsForAppUpdate(defaultContext, target); + expectSteps('noop', steps); + }); + it('should remove a dead container that is still referenced in the target state', async () => { const current = createApp({ services: [