@@ -13,6 +13,7 @@ import struct Foundation.Data
13
13
import struct Foundation. Date
14
14
import class Foundation. JSONDecoder
15
15
import class Foundation. NSError
16
+ import class Foundation. OperationQueue
16
17
import struct Foundation. URL
17
18
import TSCBasic
18
19
@@ -45,6 +46,11 @@ public struct HTTPClient {
45
46
public var configuration : HTTPClientConfiguration
46
47
private let underlying : Handler
47
48
49
+ /// DispatchSemaphore to restrict concurrent operations on manager.
50
+ private let concurrencySemaphore : DispatchSemaphore
51
+ /// OperationQueue to park pending requests
52
+ private let requestsQueue : OperationQueue
53
+
48
54
// static to share across instances of the http client
49
55
private static var hostsErrorsLock = Lock ( )
50
56
private static var hostsErrors = [ String: [ Date] ] ( )
@@ -53,6 +59,14 @@ public struct HTTPClient {
53
59
self . configuration = configuration
54
60
// FIXME: inject platform specific implementation here
55
61
self . underlying = handler ?? URLSessionHTTPClient ( ) . execute
62
+
63
+ // this queue and semaphore is used to limit the amount of concurrent http requests taking place
64
+ // the default max number of request chosen to match Concurrency.maxOperations which is the number of active CPUs
65
+ let maxConcurrentRequests = configuration. maxConcurrentRequests ?? Concurrency . maxOperations
66
+ self . requestsQueue = OperationQueue ( )
67
+ self . requestsQueue. name = " org.swift.swiftpm.http-client "
68
+ self . requestsQueue. maxConcurrentOperationCount = maxConcurrentRequests
69
+ self . concurrencySemaphore = DispatchSemaphore ( value: maxConcurrentRequests)
56
70
}
57
71
58
72
/// Execute an HTTP request asynchronously
@@ -100,12 +114,14 @@ public struct HTTPClient {
100
114
observabilityScope: observabilityScope,
101
115
progress: progress. map { handler in
102
116
{ received, expected in
117
+ // call back on the requested queue
103
118
callbackQueue. async {
104
119
handler ( received, expected)
105
120
}
106
121
}
107
122
} ,
108
123
completion: { result in
124
+ // call back on the requested queue
109
125
callbackQueue. async {
110
126
completion ( result)
111
127
}
@@ -114,45 +130,65 @@ public struct HTTPClient {
114
130
}
115
131
116
132
private func _execute( request: Request , requestNumber: Int , observabilityScope: ObservabilityScope ? , progress: ProgressHandler ? , completion: @escaping CompletionHandler ) {
117
- if self . shouldCircuitBreak ( request: request) {
118
- observabilityScope? . emit ( warning: " Circuit breaker triggered for \( request. url) " )
119
- return completion ( . failure( HTTPClientError . circuitBreakerTriggered) )
133
+ // wrap completion handler with concurrency control cleanup
134
+ let originalCompletion = completion
135
+ let completion : CompletionHandler = { result in
136
+ // free concurrency control semaphore
137
+ self . concurrencySemaphore. signal ( )
138
+ originalCompletion ( result)
120
139
}
121
140
122
- self . underlying (
123
- request,
124
- { received, expected in
125
- if let max = request. options. maximumResponseSizeInBytes {
126
- guard received < max else {
127
- // FIXME: cancel the request?
128
- return completion ( . failure( HTTPClientError . responseTooLarge ( received) ) )
129
- }
130
- }
131
- progress ? ( received, expected)
132
- } ,
133
- { result in
134
- switch result {
135
- case . failure( let error) :
136
- completion ( . failure( error) )
137
- case . success( let response) :
138
- // record host errors for circuit breaker
139
- self . recordErrorIfNecessary ( response: response, request: request)
140
- // handle retry strategy
141
- if let retryDelay = self . shouldRetry ( response: response, request: request, requestNumber: requestNumber) {
142
- observabilityScope? . emit ( warning: " \( request. url) failed, retrying in \( retryDelay) " )
143
- // TODO: dedicated retry queue?
144
- return self . configuration. callbackQueue. asyncAfter ( deadline: . now( ) + retryDelay) {
145
- self . _execute ( request: request, requestNumber: requestNumber + 1 , observabilityScope: observabilityScope, progress: progress, completion: completion)
141
+ // we must not block the calling thread (for concurrency control) so nesting this in a queue
142
+ self . requestsQueue. addOperation {
143
+ // park the request thread based on the max concurrency allowed
144
+ self . concurrencySemaphore. wait ( )
145
+
146
+ // apply circuit breaker if necessary
147
+ if self . shouldCircuitBreak ( request: request) {
148
+ observabilityScope? . emit ( warning: " Circuit breaker triggered for \( request. url) " )
149
+ return completion ( . failure( HTTPClientError . circuitBreakerTriggered) )
150
+ }
151
+
152
+ // call underlying handler
153
+ self . underlying (
154
+ request,
155
+ { received, expected in
156
+ if let max = request. options. maximumResponseSizeInBytes {
157
+ guard received < max else {
158
+ // FIXME: cancel the request?
159
+ return completion ( . failure( HTTPClientError . responseTooLarge ( received) ) )
146
160
}
147
161
}
148
- // check for valid response codes
149
- if let validResponseCodes = request. options. validResponseCodes, !validResponseCodes. contains ( response. statusCode) {
150
- return completion ( . failure( HTTPClientError . badResponseStatusCode ( response. statusCode) ) )
162
+ progress ? ( received, expected)
163
+ } ,
164
+ { result in
165
+ // handle result
166
+ switch result {
167
+ case . failure( let error) :
168
+ completion ( . failure( error) )
169
+ case . success( let response) :
170
+ // record host errors for circuit breaker
171
+ self . recordErrorIfNecessary ( response: response, request: request)
172
+ // handle retry strategy
173
+ if let retryDelay = self . shouldRetry ( response: response, request: request, requestNumber: requestNumber) {
174
+ observabilityScope? . emit ( warning: " \( request. url) failed, retrying in \( retryDelay) " )
175
+ // free concurrency control semaphore, since we re-submitting the request with the original completion handler
176
+ // using the wrapped completion handler may lead to starving the mac concurrent requests
177
+ self . concurrencySemaphore. signal ( )
178
+ // TODO: dedicated retry queue?
179
+ return self . configuration. callbackQueue. asyncAfter ( deadline: . now( ) + retryDelay) {
180
+ self . _execute ( request: request, requestNumber: requestNumber + 1 , observabilityScope: observabilityScope, progress: progress, completion: originalCompletion)
181
+ }
182
+ }
183
+ // check for valid response codes
184
+ if let validResponseCodes = request. options. validResponseCodes, !validResponseCodes. contains ( response. statusCode) {
185
+ return completion ( . failure( HTTPClientError . badResponseStatusCode ( response. statusCode) ) )
186
+ }
187
+ completion ( . success( response) )
151
188
}
152
- completion ( . success( response) )
153
189
}
154
- }
155
- )
190
+ )
191
+ }
156
192
}
157
193
158
194
private func shouldRetry( response: Response , request: Request , requestNumber: Int ) -> DispatchTimeInterval ? {
@@ -245,6 +281,7 @@ public struct HTTPClientConfiguration {
245
281
public var authorizationProvider : HTTPClientAuthorizationProvider ?
246
282
public var retryStrategy : HTTPClientRetryStrategy ?
247
283
public var circuitBreakerStrategy : HTTPClientCircuitBreakerStrategy ?
284
+ public var maxConcurrentRequests : Int ?
248
285
public var callbackQueue : DispatchQueue
249
286
250
287
public init ( ) {
@@ -253,6 +290,7 @@ public struct HTTPClientConfiguration {
253
290
self . authorizationProvider = . none
254
291
self . retryStrategy = . none
255
292
self . circuitBreakerStrategy = . none
293
+ self . maxConcurrentRequests = . none
256
294
self . callbackQueue = . sharedConcurrent
257
295
}
258
296
}
0 commit comments