Fix container auto-delete on rapid stop/start (#841)

Fixes #833.

Currently, when stopping and immediately restarting a container, it would fail with the error:
`“container expected to be in created state, got: shuttingDown”` and then be automatically deleted.
The `SandboxService` process waits five seconds before exiting after shutdown. During this interval, a rapid restart could reconnect to the still-terminating process in the `shuttingDown` state, triggering a state validation error.

This fix forcefully terminates the `SandboxService` process with `SIGKILL` upon container exit, instead of waiting five seconds. The bootstrap now defensively checks for and cleans up any stale services before registering new ones, preventing reconnections to processes in the `shuttingDown` state.
This commit is contained in:
Raj
2026-01-01 15:10:39 +05:30
committed by GitHub
parent 5064b0ffd5
commit edadf155cd
2 changed files with 28 additions and 36 deletions
@@ -90,19 +90,12 @@ public actor ContainersService {
)
)
results[config.id] = state
let plugin = runtimePlugins.first { $0.name == config.runtimeHandler }
guard let plugin else {
guard runtimePlugins.first(where: { $0.name == config.runtimeHandler }) != nil else {
throw ContainerizationError(
.internalError,
message: "failed to find runtime plugin \(config.runtimeHandler)"
)
}
try Self.registerService(
plugin: plugin,
loader: loader,
configuration: config,
path: dir
)
} catch {
try? FileManager.default.removeItem(at: dir)
log.warning("failed to load container bundle at \(dir.path)")
@@ -229,10 +222,7 @@ public actor ContainersService {
)
}
let runtimePlugin = self.runtimePlugins.filter {
$0.name == configuration.runtimeHandler
}.first
guard let runtimePlugin else {
guard self.runtimePlugins.first(where: { $0.name == configuration.runtimeHandler }) != nil else {
throw ContainerizationError(
.notFound,
message: "unable to locate runtime plugin \(configuration.runtimeHandler)"
@@ -255,13 +245,6 @@ public actor ContainersService {
try bundle.setContainerRootFs(cloning: imageFs)
try bundle.write(filename: "options.json", value: options)
try Self.registerService(
plugin: runtimePlugin,
loader: self.pluginLoader,
configuration: configuration,
path: path
)
let snapshot = ContainerSnapshot(
configuration: configuration,
status: .stopped,
@@ -293,6 +276,16 @@ public actor ContainersService {
return
}
let path = self.containerRoot.appendingPathComponent(id)
let bundle = ContainerClient.Bundle(path: path)
let config = try bundle.configuration
try Self.registerService(
plugin: self.runtimePlugins.first { $0.name == config.runtimeHandler }!,
loader: self.pluginLoader,
configuration: config,
path: path
)
let runtime = state.snapshot.configuration.runtimeHandler
let sandboxClient = try await SandboxClient.create(
id: id,
@@ -536,15 +529,23 @@ public actor ContainersService {
await self.exitMonitor.stopTracking(id: id)
// Try and shutdown the runtime helper.
do {
self.log.info("Shutting down sandbox service for \(id)")
// Shutdown and deregister the sandbox service
self.log.info("Shutting down sandbox service for \(id)")
let client = try state.getClient()
try await client.shutdown()
} catch {
self.log.error("failed to shutdown sandbox service for \(id): \(error)")
}
let path = self.containerRoot.appendingPathComponent(id)
let bundle = ContainerClient.Bundle(path: path)
let config = try bundle.configuration
let label = Self.fullLaunchdServiceLabel(
runtimeName: config.runtimeHandler,
instanceId: id
)
let client = try state.getClient()
try await client.shutdown()
// Deregister the service, launchd will terminate the process
try ServiceManager.deregister(fullServiceLabel: label)
self.log.info("Deregistered sandbox service for \(id)")
state.snapshot.status = .stopped
state.snapshot.networks = []
@@ -310,15 +310,6 @@ public actor SandboxService {
case .created, .stopped(_), .stopping:
await self.setState(.shuttingDown)
Task {
do {
try await Task.sleep(for: .seconds(5))
} catch {
self.log.error("failed to sleep before shutting down SandboxService: \(error)")
}
self.log.info("Shutting down SandboxService")
exit(0)
}
default:
throw ContainerizationError(
.invalidState,