Skip to content

Commit 0d3f20c

Browse files
authored
Fix race condition in ContainersService.create() (#721)
Fixes #692 This PR resolves a race condition in `ContainersService.create()` that occurs when creating containers with the same name concurrently.
1 parent 23b058c commit 0d3f20c

File tree

1 file changed

+64
-62
lines changed

1 file changed

+64
-62
lines changed

Sources/Services/ContainerAPIService/Containers/ContainersService.swift

Lines changed: 64 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -130,80 +130,82 @@ public actor ContainersService {
130130
public func create(configuration: ContainerConfiguration, kernel: Kernel, options: ContainerCreateOptions) async throws {
131131
self.log.debug("\(#function)")
132132

133-
guard containers[configuration.id] == nil else {
134-
throw ContainerizationError(
135-
.exists,
136-
message: "container already exists: \(configuration.id)"
137-
)
138-
}
139-
140-
var allHostnames = Set<String>()
141-
for container in containers.values {
142-
for attachmentConfiguration in container.snapshot.configuration.networks {
143-
allHostnames.insert(attachmentConfiguration.options.hostname)
133+
try await self.lock.withLock { context in
134+
guard await self.containers[configuration.id] == nil else {
135+
throw ContainerizationError(
136+
.exists,
137+
message: "container already exists: \(configuration.id)"
138+
)
144139
}
145-
}
146140

147-
var conflictingHostnames = [String]()
148-
for attachmentConfiguration in configuration.networks {
149-
if allHostnames.contains(attachmentConfiguration.options.hostname) {
150-
conflictingHostnames.append(attachmentConfiguration.options.hostname)
141+
var allHostnames = Set<String>()
142+
for container in await self.containers.values {
143+
for attachmentConfiguration in container.snapshot.configuration.networks {
144+
allHostnames.insert(attachmentConfiguration.options.hostname)
145+
}
151146
}
152-
}
153147

154-
guard conflictingHostnames.isEmpty else {
155-
throw ContainerizationError(
156-
.exists,
157-
message: "hostname(s) already exist: \(conflictingHostnames)"
158-
)
159-
}
148+
var conflictingHostnames = [String]()
149+
for attachmentConfiguration in configuration.networks {
150+
if allHostnames.contains(attachmentConfiguration.options.hostname) {
151+
conflictingHostnames.append(attachmentConfiguration.options.hostname)
152+
}
153+
}
160154

161-
let runtimePlugin = self.runtimePlugins.filter {
162-
$0.name == configuration.runtimeHandler
163-
}.first
164-
guard let runtimePlugin else {
165-
throw ContainerizationError(
166-
.notFound,
167-
message: "unable to locate runtime plugin \(configuration.runtimeHandler)"
168-
)
169-
}
155+
guard conflictingHostnames.isEmpty else {
156+
throw ContainerizationError(
157+
.exists,
158+
message: "hostname(s) already exist: \(conflictingHostnames)"
159+
)
160+
}
170161

171-
let path = self.containerRoot.appendingPathComponent(configuration.id)
172-
let systemPlatform = kernel.platform
173-
let initFs = try await getInitBlock(for: systemPlatform.ociPlatform())
162+
let runtimePlugin = self.runtimePlugins.filter {
163+
$0.name == configuration.runtimeHandler
164+
}.first
165+
guard let runtimePlugin else {
166+
throw ContainerizationError(
167+
.notFound,
168+
message: "unable to locate runtime plugin \(configuration.runtimeHandler)"
169+
)
170+
}
174171

175-
let bundle = try ContainerClient.Bundle.create(
176-
path: path,
177-
initialFilesystem: initFs,
178-
kernel: kernel,
179-
containerConfiguration: configuration
180-
)
181-
do {
182-
let containerImage = ClientImage(description: configuration.image)
183-
let imageFs = try await containerImage.getCreateSnapshot(platform: configuration.platform)
184-
try bundle.setContainerRootFs(cloning: imageFs)
185-
try bundle.write(filename: "options.json", value: options)
186-
187-
try Self.registerService(
188-
plugin: runtimePlugin,
189-
loader: self.pluginLoader,
190-
configuration: configuration,
191-
path: path
192-
)
172+
let path = self.containerRoot.appendingPathComponent(configuration.id)
173+
let systemPlatform = kernel.platform
174+
let initFs = try await self.getInitBlock(for: systemPlatform.ociPlatform())
193175

194-
let snapshot = ContainerSnapshot(
195-
configuration: configuration,
196-
status: .stopped,
197-
networks: []
176+
let bundle = try ContainerClient.Bundle.create(
177+
path: path,
178+
initialFilesystem: initFs,
179+
kernel: kernel,
180+
containerConfiguration: configuration
198181
)
199-
self.containers[configuration.id] = ContainerState(snapshot: snapshot)
200-
} catch {
201182
do {
202-
try bundle.delete()
183+
let containerImage = ClientImage(description: configuration.image)
184+
let imageFs = try await containerImage.getCreateSnapshot(platform: configuration.platform)
185+
try bundle.setContainerRootFs(cloning: imageFs)
186+
try bundle.write(filename: "options.json", value: options)
187+
188+
try Self.registerService(
189+
plugin: runtimePlugin,
190+
loader: self.pluginLoader,
191+
configuration: configuration,
192+
path: path
193+
)
194+
195+
let snapshot = ContainerSnapshot(
196+
configuration: configuration,
197+
status: .stopped,
198+
networks: []
199+
)
200+
await self.setContainerState(configuration.id, ContainerState(snapshot: snapshot), context: context)
203201
} catch {
204-
self.log.error("failed to delete bundle for container \(configuration.id): \(error)")
202+
do {
203+
try bundle.delete()
204+
} catch {
205+
self.log.error("failed to delete bundle for container \(configuration.id): \(error)")
206+
}
207+
throw error
205208
}
206-
throw error
207209
}
208210
}
209211

0 commit comments

Comments
 (0)