-
Notifications
You must be signed in to change notification settings - Fork 546
Automatically clean up anonymous volumes for containers with --rm flag #839
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -434,12 +434,17 @@ public actor ContainersService { | |
| } | ||
|
|
||
| private func handleContainerExit(id: String, code: ExitStatus? = nil) async throws { | ||
| try await self.lock.withLock { [self] context in | ||
| let shouldCleanupVolumes = try await self.lock.withLock { [self] context in | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious why we don't just do the cleanup in handleContainerExit itself
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once the lock is released if autoRemove was true, the container bundle has already been deleted by cleanup(), so we can't call getContainerCreationOptions() again to check the flag, the file is gone. So we need to capture the decision while the container still exists (inside the lock) then act on it outside the lock
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I mean is if cleanup() is called we know that autoRemove was supplied, so why not just do it in there? |
||
| try await handleContainerExit(id: id, code: code, context: context) | ||
| } | ||
|
|
||
| // Lock released, safe for XPC calls | ||
| if shouldCleanupVolumes { | ||
| await self.cleanupAnonymousVolumes(forContainer: id) | ||
| } | ||
| } | ||
|
|
||
| private func handleContainerExit(id: String, code: ExitStatus?, context: AsyncLock.Context) async throws { | ||
| private func handleContainerExit(id: String, code: ExitStatus?, context: AsyncLock.Context) async throws -> Bool { | ||
| if let code { | ||
| self.log.info("Handling container \(id) exit. Code \(code)") | ||
| } | ||
|
|
@@ -448,11 +453,11 @@ public actor ContainersService { | |
| do { | ||
| state = try self.getContainerState(id: id, context: context) | ||
| if state.snapshot.status == .stopped { | ||
| return | ||
| return false | ||
| } | ||
| } catch { | ||
| // Was auto removed by the background thread, nothing for us to do. | ||
| return | ||
| return false | ||
| } | ||
|
|
||
| await self.exitMonitor.stopTracking(id: id) | ||
|
|
@@ -475,7 +480,9 @@ public actor ContainersService { | |
| let options = try getContainerCreationOptions(id: id) | ||
| if options.autoRemove { | ||
| try await self.cleanup(id: id, context: context) | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| private static func fullLaunchdServiceLabel(runtimeName: String, instanceId: String) -> String { | ||
|
|
@@ -511,6 +518,55 @@ public actor ContainersService { | |
| try await self._cleanup(id: id) | ||
| } | ||
|
|
||
| /// Clean up anonymous volumes created by a container | ||
| private func cleanupAnonymousVolumes(forContainer id: String) async { | ||
| do { | ||
| let allVolumes = try await ClientVolume.list() | ||
| let anonymousVolumes = allVolumes.filter { $0.isAnonymous && $0.createdByContainerID == id } | ||
|
|
||
| guard !anonymousVolumes.isEmpty else { return } | ||
|
|
||
| await withTaskGroup(of: (String, Error?).self) { group in | ||
| for volume in anonymousVolumes { | ||
| group.addTask { | ||
| do { | ||
| try await ClientVolume.delete(name: volume.name) | ||
| return (volume.name, nil) | ||
| } catch { | ||
| return (volume.name, error) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for await (volumeName, error) in group { | ||
| if let error = error { | ||
| self.log.warning( | ||
| "Failed to delete anonymous volume", | ||
| metadata: [ | ||
| "volume": "\(volumeName)", | ||
| "container": "\(id)", | ||
| "error": "\(error)", | ||
| ]) | ||
| } else { | ||
| self.log.info( | ||
| "Deleted anonymous volume", | ||
| metadata: [ | ||
| "volume": "\(volumeName)", | ||
| "container": "\(id)", | ||
| ]) | ||
| } | ||
| } | ||
| } | ||
| } catch { | ||
| self.log.error( | ||
| "Failed to query anonymous volumes for cleanup", | ||
| metadata: [ | ||
| "container": "\(id)", | ||
| "error": "\(error)", | ||
| ]) | ||
| } | ||
| } | ||
|
|
||
| private func getContainerCreationOptions(id: String) throws -> ContainerCreateOptions { | ||
| let path = self.containerRoot.appendingPathComponent(id) | ||
| let bundle = ContainerClient.Bundle(path: path) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern with this approach is that the ID of a container isn't really a unique ID across all time. In this case we might be able to get by with it but in general if you store the ID
test, that could be one container on one day and a totally different container a week later.Would I make more sense for us, when a container is removed in the API server, to quickly scan all containers for volume references, find all unreferenced volumes, and then prune those that are marked anonymous? Given what you have in
cleanupAnonymousVolumes()this might be a simple change?