Skip to content

Commit fdd4fbd

Browse files
committed
Handle concurrent removal (microsoft/vscode-remote-release#6509)
1 parent 11587da commit fdd4fbd

File tree

5 files changed

+70
-8
lines changed

5 files changed

+70
-8
lines changed

src/spec-node/dockerCompose.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ContainerProperties, setupInContainer, ResolverProgress } from '../spec
1111
import { ContainerError } from '../spec-common/errors';
1212
import { Workspace } from '../spec-utils/workspaces';
1313
import { equalPaths, parseVersion, isEarlierVersion, CLIHost } from '../spec-common/commonUtils';
14-
import { ContainerDetails, inspectContainer, listContainers, DockerCLIParameters, dockerCLI, dockerComposeCLI, dockerComposePtyCLI, PartialExecParameters, DockerComposeCLI, ImageDetails, toExecParameters, toPtyExecParameters } from '../spec-shutdown/dockerUtils';
14+
import { ContainerDetails, inspectContainer, listContainers, DockerCLIParameters, dockerComposeCLI, dockerComposePtyCLI, PartialExecParameters, DockerComposeCLI, ImageDetails, toExecParameters, toPtyExecParameters, removeContainer } from '../spec-shutdown/dockerUtils';
1515
import { DevContainerFromDockerComposeConfig, getDockerComposeFilePaths } from '../spec-configuration/configuration';
1616
import { Log, LogLevel, makeLog, terminalEscapeSequences } from '../spec-utils/log';
1717
import { getExtendImageBuildInfo, updateRemoteUserUID } from './containerFeatures';
@@ -54,7 +54,7 @@ async function _openDockerComposeDevContainer(params: DockerResolverParameters,
5454
if (container && (params.removeOnStartup === true || params.removeOnStartup === container.Id)) {
5555
const text = 'Removing existing container.';
5656
const start = common.output.start(text);
57-
await dockerCLI(params, 'rm', '-f', container.Id);
57+
await removeContainer(params, container.Id);
5858
common.output.stop(text, start);
5959
container = undefined;
6060
}

src/spec-node/singleContainer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, getDockerfilePath, getDockerContextPath, DockerResolverParameters, isDockerFileConfig, uriToWSLFsPath, WorkspaceConfiguration, getFolderImageName, inspectDockerImage, logUMask, SubstitutedConfig, checkDockerSupportForGPU, isBuildKitImagePolicyError } from './utils';
88
import { ContainerProperties, setupInContainer, ResolverProgress, ResolverParameters } from '../spec-common/injectHeadless';
99
import { ContainerError, toErrorText } from '../spec-common/errors';
10-
import { ContainerDetails, listContainers, DockerCLIParameters, inspectContainers, dockerCLI, dockerPtyCLI, toPtyExecParameters, ImageDetails, toExecParameters } from '../spec-shutdown/dockerUtils';
10+
import { ContainerDetails, listContainers, DockerCLIParameters, inspectContainers, dockerCLI, dockerPtyCLI, toPtyExecParameters, ImageDetails, toExecParameters, removeContainer } from '../spec-shutdown/dockerUtils';
1111
import { DevContainerConfig, DevContainerFromDockerfileConfig, DevContainerFromImageConfig } from '../spec-configuration/configuration';
1212
import { LogLevel, Log, makeLog } from '../spec-utils/log';
1313
import { extendImage, getExtendImageBuildInfo, updateRemoteUserUID } from './containerFeatures';
@@ -299,7 +299,7 @@ export async function findExistingContainer(params: DockerResolverParameters, la
299299
if (container && (params.removeOnStartup === true || params.removeOnStartup === container.Id)) {
300300
const text = 'Removing Existing Container';
301301
const start = common.output.start(text);
302-
await dockerCLI(params, 'rm', '-f', container.Id);
302+
await removeContainer(params, container.Id);
303303
common.output.stop(text, start);
304304
container = undefined;
305305
}

src/spec-node/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { CommonDevContainerConfig, ContainerProperties, getContainerProperties,
1515
import { Workspace } from '../spec-utils/workspaces';
1616
import { URI } from 'vscode-uri';
1717
import { ShellServer } from '../spec-common/shellServer';
18-
import { inspectContainer, inspectImage, getEvents, ContainerDetails, DockerCLIParameters, dockerExecFunction, dockerPtyCLI, dockerPtyExecFunction, toDockerImageName, DockerComposeCLI, ImageDetails, dockerCLI } from '../spec-shutdown/dockerUtils';
18+
import { inspectContainer, inspectImage, getEvents, ContainerDetails, DockerCLIParameters, dockerExecFunction, dockerPtyCLI, dockerPtyExecFunction, toDockerImageName, DockerComposeCLI, ImageDetails, dockerCLI, removeContainer } from '../spec-shutdown/dockerUtils';
1919
import { getRemoteWorkspaceFolder } from './dockerCompose';
2020
import { findGitRootFolder } from '../spec-common/git';
2121
import { parentURI, uriToFsPath } from '../spec-configuration/configurationCommonUtils';
@@ -564,7 +564,7 @@ export async function findContainerAndIdLabels(params: DockerResolverParameters
564564
container = undefined;
565565
} else if (removeContainerWithOldLabels === true || removeContainerWithOldLabels === container.Id) {
566566
// Remove container, so it will be rebuilt with new labels.
567-
await dockerCLI(params, 'rm', '-f', container.Id);
567+
await removeContainer(params, container.Id);
568568
container = undefined;
569569
}
570570
}

src/spec-shutdown/dockerUtils.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as ptyType from 'node-pty';
99
import { Log, makeLog } from '../spec-utils/log';
1010
import { Event } from '../spec-utils/event';
1111
import { escapeRegExCharacters } from '../spec-utils/strings';
12+
import { delay } from '../spec-common/async';
1213

1314
export interface ContainerDetails {
1415
Id: string;
@@ -168,15 +169,52 @@ export async function listContainers(params: DockerCLIParameters | PartialExecPa
168169
.filter(s => !!s);
169170
}
170171

171-
export async function getEvents(params: DockerResolverParameters, filters?: Record<string, string[]>) {
172+
export async function removeContainer(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, nameOrId: string) {
173+
let eventsProcess: Exec | undefined;
174+
let removedSeenP: Promise<void> | undefined;
175+
try {
176+
for (let i = 0, n = 4; i < n; i++) {
177+
try {
178+
await dockerCLI(params, 'rm', '-f', nameOrId);
179+
return;
180+
} catch (err) {
181+
// https://github.com/microsoft/vscode-remote-release/issues/6509
182+
const stderr: string = err?.stderr?.toString().toLowerCase() || '';
183+
if (i === n - 1 || !stderr.includes('already in progress')) {
184+
throw err;
185+
}
186+
if (!removedSeenP) {
187+
eventsProcess = await getEvents(params, {
188+
container: [nameOrId],
189+
event: ['destroy'],
190+
});
191+
removedSeenP = new Promise<void>(resolve => {
192+
eventsProcess!.stdout.on('data', () => {
193+
resolve();
194+
eventsProcess!.terminate();
195+
removedSeenP = new Promise(() => {}); // safeguard in case we see the 'removal already in progress' error again
196+
});
197+
});
198+
}
199+
await Promise.race([removedSeenP, delay(1000)]);
200+
}
201+
}
202+
} finally {
203+
if (eventsProcess) {
204+
eventsProcess.terminate();
205+
}
206+
}
207+
}
208+
209+
export async function getEvents(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, filters?: Record<string, string[]>) {
172210
const { exec, cmd, args, env, output } = toExecParameters(params);
173211
const filterArgs = [];
174212
for (const filter in filters) {
175213
for (const value of filters[filter]) {
176214
filterArgs.push('--filter', `${filter}=${value}`);
177215
}
178216
}
179-
const format = params.isPodman ? 'json' : '{{json .}}'; // https://github.com/containers/libpod/issues/5981
217+
const format = 'isPodman' in params && params.isPodman ? 'json' : '{{json .}}'; // https://github.com/containers/libpod/issues/5981
180218
const combinedArgs = (args || []).concat(['events', '--format', format, ...filterArgs]);
181219

182220
const p = await exec({

src/test/dockerUtils.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import { createPlainLog, LogLevel, makeLog } from '../spec-utils/log';
77
import { inspectImageInRegistry, qualifyImageName } from '../spec-node/utils';
88
import assert from 'assert';
9+
import { dockerCLI, listContainers, PartialExecParameters, removeContainer, toExecParameters } from '../spec-shutdown/dockerUtils';
10+
import { createCLIParams } from './testUtils';
911

1012
export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace));
1113

@@ -46,4 +48,26 @@ describe('Docker utils', function () {
4648
assert.strictEqual(qualifyImageName('random/image'), 'docker.io/random/image');
4749
assert.strictEqual(qualifyImageName('foo/random/image'), 'foo/random/image');
4850
});
51+
52+
it('protects against concurrent removal', async () => {
53+
const params = await createCLIParams(__dirname);
54+
const verboseParams = { ...toExecParameters(params), output: makeLog(output, LogLevel.Info), print: 'continuous' as 'continuous' };
55+
const { stdout } = await dockerCLI(verboseParams, 'run', '-d', 'ubuntu:latest', 'sleep', 'inf');
56+
const containerId = stdout.toString().trim();
57+
const start = Date.now();
58+
await Promise.all([
59+
testRemoveContainer(verboseParams, containerId),
60+
testRemoveContainer(verboseParams, containerId),
61+
testRemoveContainer(verboseParams, containerId),
62+
]);
63+
console.log('removal took', Date.now() - start, 'ms');
64+
});
4965
});
66+
67+
async function testRemoveContainer(params: PartialExecParameters, nameOrId: string) {
68+
await removeContainer(params, nameOrId);
69+
const all = await listContainers(params, true);
70+
if (all.some(shortId => nameOrId.startsWith(shortId))) {
71+
throw new Error('container still exists');
72+
}
73+
}

0 commit comments

Comments
 (0)