-
Notifications
You must be signed in to change notification settings - Fork 583
Add --max-concurrent-downloads flag for parallel layer downloads #716
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
Changes from 8 commits
53e6c45
903beb8
2c97436
21ac59f
9659c53
b2e13d9
efa7b87
341c918
ae30d88
c905f84
2029f72
e1c03a2
3374833
b9e673a
7359006
0996e40
04c9d8e
f51d64b
5cacdd6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -214,5 +214,8 @@ public struct Flags { | |
|
|
||
| @Option(name: .long, help: ArgumentHelp("Progress type (format: none|ansi)", valueName: "type")) | ||
| public var progress: ProgressType = .ansi | ||
|
|
||
| @Option(name: .long, help: "Maximum number of concurrent layer downloads (default: 3)") | ||
|
||
| public var maxConcurrentDownloads: Int = 3 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| #!/usr/bin/env swift | ||
dkovba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
dkovba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| import Foundation | ||
|
|
||
| func testConcurrentDownloads() async throws { | ||
| print("Testing concurrent download behavior...\n") | ||
|
|
||
| // Track concurrent task count | ||
| actor ConcurrencyTracker { | ||
| var currentCount = 0 | ||
| var maxObservedCount = 0 | ||
| var completedTasks = 0 | ||
|
|
||
| func taskStarted() { | ||
| currentCount += 1 | ||
| maxObservedCount = max(maxObservedCount, currentCount) | ||
| } | ||
|
|
||
| func taskCompleted() { | ||
| currentCount -= 1 | ||
| completedTasks += 1 | ||
| } | ||
|
|
||
| func getStats() -> (max: Int, completed: Int) { | ||
| return (maxObservedCount, completedTasks) | ||
| } | ||
|
|
||
| func reset() { | ||
| currentCount = 0 | ||
| maxObservedCount = 0 | ||
| completedTasks = 0 | ||
| } | ||
| } | ||
|
|
||
| let tracker = ConcurrencyTracker() | ||
|
|
||
| // Test with different concurrency limits | ||
| for maxConcurrent in [1, 3, 6] { | ||
| await tracker.reset() | ||
|
|
||
| // Simulate downloading 20 layers | ||
| let layerCount = 20 | ||
| let layers = Array(0..<layerCount) | ||
|
|
||
| print("Testing maxConcurrent=\(maxConcurrent) with \(layerCount) layers...") | ||
|
|
||
| let startTime = Date() | ||
|
|
||
| try await withThrowingTaskGroup(of: Void.self) { group in | ||
| var iterator = layers.makeIterator() | ||
|
|
||
| // Start initial batch based on maxConcurrent | ||
| for _ in 0..<maxConcurrent { | ||
| if iterator.next() != nil { | ||
| group.addTask { | ||
| await tracker.taskStarted() | ||
| try await Task.sleep(nanoseconds: 10_000_000) | ||
| await tracker.taskCompleted() | ||
| } | ||
| } | ||
| } | ||
| for try await _ in group { | ||
| if iterator.next() != nil { | ||
| group.addTask { | ||
| await tracker.taskStarted() | ||
| try await Task.sleep(nanoseconds: 10_000_000) | ||
| await tracker.taskCompleted() | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let duration = Date().timeIntervalSince(startTime) | ||
| let stats = await tracker.getStats() | ||
|
|
||
| print(" ✓ Completed: \(stats.completed)/\(layerCount)") | ||
| print(" ✓ Max concurrent: \(stats.max)") | ||
| print(" ✓ Duration: \(String(format: "%.3f", duration))s") | ||
|
|
||
| guard stats.max <= maxConcurrent + 1 else { | ||
| throw TestError.concurrencyLimitExceeded | ||
| } | ||
|
|
||
| guard stats.completed == layerCount else { | ||
| throw TestError.incompleteTasks | ||
| } | ||
|
|
||
| print(" ✅ PASSED\n") | ||
| } | ||
|
|
||
| print("All tests passed!") | ||
| } | ||
|
|
||
| enum TestError: Error { | ||
| case concurrencyLimitExceeded | ||
| case incompleteTasks | ||
| } | ||
|
|
||
| Task { | ||
| do { | ||
| try await testConcurrentDownloads() | ||
| exit(0) | ||
| } catch { | ||
| print("Test failed: \(error)") | ||
| exit(1) | ||
| } | ||
| } | ||
|
|
||
| RunLoop.main.run() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| #!/usr/bin/env swift | ||
|
|
||
| import Foundation | ||
|
|
||
| print("Testing parameter flow...\n") | ||
|
|
||
| print("1. CLI flag parsing...") | ||
| struct ProgressFlags { | ||
| var disableProgressUpdates = false | ||
| var maxConcurrentDownloads: Int = 3 | ||
| } | ||
|
|
||
| let defaultFlags = ProgressFlags() | ||
| print(" ✓ Default: \(defaultFlags.maxConcurrentDownloads)") | ||
|
|
||
| let customFlags = ProgressFlags(disableProgressUpdates: false, maxConcurrentDownloads: 6) | ||
| print(" ✓ Custom: \(customFlags.maxConcurrentDownloads)") | ||
| print(" PASSED\n") | ||
|
|
||
| print("2. XPC key...") | ||
| enum ImageServiceXPCKeys: String { | ||
| case maxConcurrentDownloads | ||
| } | ||
|
|
||
| let key = ImageServiceXPCKeys.maxConcurrentDownloads | ||
| print(" ✓ Key exists: \(key.rawValue)") | ||
| print(" PASSED\n") | ||
|
|
||
| print("3. Function signatures...") | ||
| func mockClientImagePull( | ||
| reference: String, | ||
| maxConcurrentDownloads: Int = 3 | ||
| ) -> String { | ||
| return "pull(\(reference), maxConcurrent=\(maxConcurrentDownloads))" | ||
| } | ||
|
|
||
| _ = mockClientImagePull(reference: "nginx:latest") | ||
| _ = mockClientImagePull(reference: "nginx:latest", maxConcurrentDownloads: 6) | ||
| print(" ✓ Compiles") | ||
| print(" PASSED\n") | ||
|
|
||
| print("4. Parameter propagation...") | ||
|
|
||
| struct MockXPCMessage { | ||
| var values: [String: Any] = [:] | ||
|
|
||
| mutating func set(key: String, value: Int64) { | ||
| values[key] = value | ||
| } | ||
|
|
||
| func int64(key: String) -> Int64 { | ||
| return values[key] as? Int64 ?? 3 | ||
| } | ||
| } | ||
|
|
||
| func simulateFlow(maxConcurrent: Int) -> Int { | ||
| let flags = ProgressFlags(maxConcurrentDownloads: maxConcurrent) | ||
| var xpcMessage = MockXPCMessage() | ||
| xpcMessage.set(key: "maxConcurrentDownloads", value: Int64(flags.maxConcurrentDownloads)) | ||
| return Int(xpcMessage.int64(key: "maxConcurrentDownloads")) | ||
| } | ||
|
|
||
| for testValue in [1, 3, 6] { | ||
| guard simulateFlow(maxConcurrent: testValue) == testValue else { | ||
| print(" ✗ Failed") | ||
| exit(1) | ||
| } | ||
| } | ||
| print(" ✓ Values propagate correctly") | ||
| print(" PASSED\n") | ||
|
|
||
| print("5. Implementation verification...") | ||
|
|
||
| let filesToCheck = [ | ||
| "Sources/ContainerClient/Flags.swift", | ||
| "Sources/ContainerClient/Core/ClientImage.swift", | ||
| "Sources/Services/ContainerImagesService/Server/ImageService.swift", | ||
| ] | ||
|
|
||
| for file in filesToCheck { | ||
| if let content = try? String(contentsOf: URL(fileURLWithPath: file), encoding: .utf8), | ||
| content.contains("maxConcurrentDownloads") { | ||
| continue | ||
| } | ||
| print(" ✗ Missing in \(file)") | ||
| exit(1) | ||
| } | ||
| print(" ✓ Found in implementation") | ||
| print(" PASSED\n") | ||
|
|
||
| print("All tests passed!") |
Uh oh!
There was an error while loading. Please reload this page.