Skip to content

Commit a666ad1

Browse files
authored
Deprecate sync methods in RegistryClient (#8263)
Deprecate the synchronous methods in RegistryClient and move the logic from then sync versions in to the async versions. This async-ifys the previously callback based methods, flattening them out. The synchronous versions of these methods will now warn when they're used. After some time they can be removed completely.
1 parent 70862aa commit a666ad1

13 files changed

+1698
-2232
lines changed

Sources/Basics/Cancellator.swift

+14-8
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,13 @@ import WinSDK
1919
import Android
2020
#endif
2121

22-
public typealias CancellationHandler = @Sendable (DispatchTime) throws -> Void
22+
public typealias CancellationHandler = @Sendable (DispatchTime) async throws -> Void
2323

2424
public final class Cancellator: Cancellable, Sendable {
2525
public typealias RegistrationKey = String
2626

2727
private let observabilityScope: ObservabilityScope?
2828
private let registry = ThreadSafeKeyValueStore<String, (name: String, handler: CancellationHandler)>()
29-
private let cancelationQueue = DispatchQueue(
30-
label: "org.swift.swiftpm.cancellator",
31-
qos: .userInteractive,
32-
attributes: .concurrent
33-
)
3429
private let cancelling = ThreadSafeBox<Bool>(false)
3530

3631
private static let signalHandlerLock = NSLock()
@@ -119,6 +114,11 @@ public final class Cancellator: Cancellable, Sendable {
119114
self.register(name: name, handler: handler.cancel(deadline:))
120115
}
121116

117+
@discardableResult
118+
public func register(name: String, handler: AsyncCancellable) -> RegistrationKey? {
119+
self.register(name: name, handler: handler.cancel(deadline:))
120+
}
121+
122122
@discardableResult
123123
public func register(name: String, handler: @escaping @Sendable () throws -> Void) -> RegistrationKey? {
124124
self.register(name: name, handler: { _ in try handler() })
@@ -159,10 +159,12 @@ public final class Cancellator: Cancellable, Sendable {
159159
let cancelled = ThreadSafeArrayStore<String>()
160160
let group = DispatchGroup()
161161
for (_, (name, handler)) in cancellationHandlers {
162-
self.cancelationQueue.async(group: group) {
162+
group.enter()
163+
Task {
164+
defer { group.leave() }
163165
do {
164166
self.observabilityScope?.emit(debug: "cancelling '\(name)'")
165-
try handler(handlersDeadline)
167+
try await handler(handlersDeadline)
166168
cancelled.append(name)
167169
} catch {
168170
self.observabilityScope?.emit(
@@ -192,6 +194,10 @@ public protocol Cancellable {
192194
func cancel(deadline: DispatchTime) throws -> Void
193195
}
194196

197+
public protocol AsyncCancellable {
198+
func cancel(deadline: DispatchTime) async throws -> Void
199+
}
200+
195201
public struct CancellationError: Error, CustomStringConvertible {
196202
public let description: String
197203

Sources/Basics/HTTPClient/HTTPClient.swift

+84-33
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public actor HTTPClient {
4040
/// Array of `HostErrors` values, which is used for applying a circuit-breaking strategy.
4141
private var hostsErrors = [String: HostErrors]()
4242

43+
/// Tracks all active network request tasks.
44+
private var activeTasks: Set<Task<HTTPClient.Response, Error>> = []
45+
4346
public init(configuration: HTTPClientConfiguration = .init(), implementation: Implementation? = nil) {
4447
self.configuration = configuration
4548
self.implementation = implementation ?? URLSessionHTTPClient().execute
@@ -92,6 +95,21 @@ public actor HTTPClient {
9295
return try await self.executeWithStrategies(request: request, requestNumber: 0, observabilityScope, progress)
9396
}
9497

98+
/// Cancel all in flight network reqeusts.
99+
public func cancel(deadline: DispatchTime) async {
100+
for task in activeTasks {
101+
task.cancel()
102+
}
103+
104+
// Wait for tasks to complete or timeout
105+
while !activeTasks.isEmpty && (deadline.distance(to: .now()).nanoseconds() ?? 0) > 0 {
106+
await Task.yield()
107+
}
108+
109+
// Clear out the active task list regardless of whether they completed or not
110+
activeTasks.removeAll()
111+
}
112+
95113
private func executeWithStrategies(
96114
request: Request,
97115
requestNumber: Int,
@@ -104,46 +122,57 @@ public actor HTTPClient {
104122
throw HTTPClientError.circuitBreakerTriggered
105123
}
106124

107-
let response = try await self.tokenBucket.withToken {
108-
try await self.implementation(request) { received, expected in
109-
if let max = request.options.maximumResponseSizeInBytes {
110-
guard received < max else {
111-
// It's a responsibility of the underlying client implementation to cancel the request
112-
// when this closure throws an error
113-
throw HTTPClientError.responseTooLarge(received)
125+
let task = Task {
126+
let response = try await self.tokenBucket.withToken {
127+
try Task.checkCancellation()
128+
129+
return try await self.implementation(request) { received, expected in
130+
if let max = request.options.maximumResponseSizeInBytes {
131+
guard received < max else {
132+
// It's a responsibility of the underlying client implementation to cancel the request
133+
// when this closure throws an error
134+
throw HTTPClientError.responseTooLarge(received)
135+
}
114136
}
115-
}
116137

117-
try progress?(received, expected)
138+
try progress?(received, expected)
139+
}
118140
}
119-
}
120141

121-
self.recordErrorIfNecessary(response: response, request: request)
142+
self.recordErrorIfNecessary(response: response, request: request)
122143

123-
// handle retry strategy
124-
if let retryDelay = self.calculateRetry(
125-
response: response,
126-
request: request,
127-
requestNumber: requestNumber
128-
), let retryDelayInNanoseconds = retryDelay.nanoseconds() {
129-
observabilityScope?.emit(warning: "\(request.url) failed, retrying in \(retryDelay)")
130-
try await Task.sleep(nanoseconds: UInt64(retryDelayInNanoseconds))
131-
132-
return try await self.executeWithStrategies(
144+
// handle retry strategy
145+
if let retryDelay = self.calculateRetry(
146+
response: response,
133147
request: request,
134-
requestNumber: requestNumber + 1,
135-
observabilityScope,
136-
progress
137-
)
138-
}
139-
// check for valid response codes
140-
if let validResponseCodes = request.options.validResponseCodes,
141-
!validResponseCodes.contains(response.statusCode)
142-
{
143-
throw HTTPClientError.badResponseStatusCode(response.statusCode)
144-
} else {
145-
return response
148+
requestNumber: requestNumber
149+
), let retryDelayInNanoseconds = retryDelay.nanoseconds() {
150+
try Task.checkCancellation()
151+
152+
observabilityScope?.emit(warning: "\(request.url) failed, retrying in \(retryDelay)")
153+
try await Task.sleep(nanoseconds: UInt64(retryDelayInNanoseconds))
154+
155+
return try await self.executeWithStrategies(
156+
request: request,
157+
requestNumber: requestNumber + 1,
158+
observabilityScope,
159+
progress
160+
)
161+
}
162+
// check for valid response codes
163+
if let validResponseCodes = request.options.validResponseCodes,
164+
!validResponseCodes.contains(response.statusCode)
165+
{
166+
throw HTTPClientError.badResponseStatusCode(response.statusCode)
167+
} else {
168+
return response
169+
}
146170
}
171+
172+
activeTasks.insert(task)
173+
defer { activeTasks.remove(task) }
174+
175+
return try await task.value
147176
}
148177

149178
private func calculateRetry(response: Response, request: Request, requestNumber: Int) -> SendableTimeInterval? {
@@ -258,4 +287,26 @@ extension HTTPClient {
258287
Request(method: .delete, url: url, headers: headers, body: nil, options: options)
259288
)
260289
}
290+
291+
public func download(
292+
_ url: URL,
293+
headers: HTTPClientHeaders = .init(),
294+
options: Request.Options = .init(),
295+
progressHandler: ProgressHandler? = nil,
296+
fileSystem: FileSystem,
297+
destination: AbsolutePath,
298+
observabilityScope: ObservabilityScope? = .none
299+
) async throws -> Response {
300+
try await self.execute(
301+
Request(
302+
kind: .download(fileSystem: fileSystem, destination: destination),
303+
url: url,
304+
headers: headers,
305+
body: nil,
306+
options: options
307+
),
308+
observabilityScope: observabilityScope,
309+
progress: progressHandler
310+
)
311+
}
261312
}

Sources/PackageRegistry/ChecksumTOFU.swift

-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,6 @@ struct PackageVersionChecksumTOFU {
154154
}
155155
}
156156

157-
@available(*, noasync, message: "Use the async alternative")
158157
func validateManifest(
159158
registry: Registry,
160159
package: PackageIdentity.RegistryIdentity,

0 commit comments

Comments
 (0)