Skip to content

Commit d3ee3f6

Browse files
authored
Fix a memory leak in DownloadTaskManager and DataTaskManager (#7408)
`DownloadTaskManager` had a strong reference to `URLSession`, which retained its delegate, which was the `DownloadTaskManager`. This formed a retain cycle. Make the reference from `URLSession` to the delegate weak and only keep `DownloadTaskManager` alive as long as `DownloadTask`s need it. This causes the `DownloadTaskManager` to be deallocated once nobody holds a reference to it anymore and all the tasks it manages are finished. Finally, we need to call `invalidate` on the `URLSession` to tell it that we’re done and that it should release its delegate (which is now the `WeakDownloadTaskManager`wrapper). This retain cycle was causing a leak in sourcekit-lsp. I verified that the leak no longer exists with this patch. rdar://125012584
1 parent adb8ea0 commit d3ee3f6

File tree

1 file changed

+123
-7
lines changed

1 file changed

+123
-7
lines changed

Sources/Basics/HTTPClient/URLSessionHTTPClient.swift

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,60 @@ final class URLSessionHTTPClient {
8787
}
8888
}
8989

90-
private class DataTaskManager: NSObject, URLSessionDataDelegate {
90+
/// A weak wrapper around `DataTaskManager` that conforms to `URLSessionDataDelegate`.
91+
///
92+
/// This ensures that we don't get a retain cycle between `DataTaskManager.session` -> `URLSession.delegate` -> `DataTaskManager`.
93+
///
94+
/// The `DataTaskManager` is being kept alive by a reference from all `DataTask`s that it manages. Once all the
95+
/// `DataTasks` have finished and are deallocated, `DataTaskManager` will get deinitialized, which invalidates the
96+
/// session, which then lets go of `WeakDataTaskManager`.
97+
private class WeakDataTaskManager: NSObject, URLSessionDataDelegate {
98+
private weak var dataTaskManager: DataTaskManager?
99+
100+
init(_ dataTaskManager: DataTaskManager? = nil) {
101+
self.dataTaskManager = dataTaskManager
102+
}
103+
104+
func urlSession(
105+
_ session: URLSession,
106+
dataTask: URLSessionDataTask,
107+
didReceive response: URLResponse,
108+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
109+
) {
110+
dataTaskManager?.urlSession(
111+
session,
112+
dataTask: dataTask,
113+
didReceive: response,
114+
completionHandler: completionHandler
115+
)
116+
}
117+
118+
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
119+
dataTaskManager?.urlSession(session, dataTask: dataTask, didReceive: data)
120+
}
121+
122+
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
123+
dataTaskManager?.urlSession(session, task: task, didCompleteWithError: error)
124+
}
125+
126+
func urlSession(
127+
_ session: URLSession,
128+
task: URLSessionTask,
129+
willPerformHTTPRedirection response: HTTPURLResponse,
130+
newRequest request: URLRequest,
131+
completionHandler: @escaping (URLRequest?) -> Void
132+
) {
133+
dataTaskManager?.urlSession(
134+
session,
135+
task: task,
136+
willPerformHTTPRedirection: response,
137+
newRequest: request,
138+
completionHandler: completionHandler
139+
)
140+
}
141+
}
142+
143+
private class DataTaskManager {
91144
private var tasks = ThreadSafeKeyValueStore<Int, DataTask>()
92145
private let delegateQueue: OperationQueue
93146
private var session: URLSession!
@@ -96,8 +149,11 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate {
96149
self.delegateQueue = OperationQueue()
97150
self.delegateQueue.name = "org.swift.swiftpm.urlsession-http-client-data-delegate"
98151
self.delegateQueue.maxConcurrentOperationCount = 1
99-
super.init()
100-
self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: self.delegateQueue)
152+
self.session = URLSession(configuration: configuration, delegate: WeakDataTaskManager(self), delegateQueue: self.delegateQueue)
153+
}
154+
155+
deinit {
156+
session.finishTasksAndInvalidate()
101157
}
102158

103159
func makeTask(
@@ -110,6 +166,7 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate {
110166
self.tasks[task.taskIdentifier] = DataTask(
111167
task: task,
112168
progressHandler: progress,
169+
dataTaskManager: self,
113170
completionHandler: completion,
114171
authorizationProvider: authorizationProvider
115172
)
@@ -192,6 +249,11 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate {
192249
class DataTask {
193250
let task: URLSessionDataTask
194251
let completionHandler: LegacyHTTPClient.CompletionHandler
252+
/// A strong reference to keep the `DataTaskManager` alive so it can handle the callbacks from the
253+
/// `URLSession`.
254+
///
255+
/// See comment on `WeakDataTaskManager`.
256+
let dataTaskManager: DataTaskManager
195257
let progressHandler: LegacyHTTPClient.ProgressHandler?
196258
let authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider?
197259

@@ -202,18 +264,61 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate {
202264
init(
203265
task: URLSessionDataTask,
204266
progressHandler: LegacyHTTPClient.ProgressHandler?,
267+
dataTaskManager: DataTaskManager,
205268
completionHandler: @escaping LegacyHTTPClient.CompletionHandler,
206269
authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider?
207270
) {
208271
self.task = task
209272
self.progressHandler = progressHandler
273+
self.dataTaskManager = dataTaskManager
210274
self.completionHandler = completionHandler
211275
self.authorizationProvider = authorizationProvider
212276
}
213277
}
214278
}
215279

216-
private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
280+
/// This uses the same pattern as `WeakDataTaskManager`. See comment on that type.
281+
private class WeakDownloadTaskManager: NSObject, URLSessionDownloadDelegate {
282+
private weak var downloadTaskManager: DownloadTaskManager?
283+
284+
init(_ downloadTaskManager: DownloadTaskManager? = nil) {
285+
self.downloadTaskManager = downloadTaskManager
286+
}
287+
288+
func urlSession(
289+
_ session: URLSession,
290+
downloadTask: URLSessionDownloadTask,
291+
didWriteData bytesWritten: Int64,
292+
totalBytesWritten: Int64,
293+
totalBytesExpectedToWrite: Int64
294+
) {
295+
downloadTaskManager?.urlSession(
296+
session,
297+
downloadTask: downloadTask,
298+
didWriteData: bytesWritten,
299+
totalBytesWritten: totalBytesWritten,
300+
totalBytesExpectedToWrite: totalBytesExpectedToWrite
301+
)
302+
}
303+
304+
func urlSession(
305+
_ session: URLSession,
306+
downloadTask: URLSessionDownloadTask,
307+
didFinishDownloadingTo location: URL
308+
) {
309+
downloadTaskManager?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
310+
}
311+
312+
func urlSession(
313+
_ session: URLSession,
314+
task downloadTask: URLSessionTask,
315+
didCompleteWithError error: Error?
316+
) {
317+
downloadTaskManager?.urlSession(session, task: downloadTask, didCompleteWithError: error)
318+
}
319+
}
320+
321+
private class DownloadTaskManager {
217322
private var tasks = ThreadSafeKeyValueStore<Int, DownloadTask>()
218323
private let delegateQueue: OperationQueue
219324
private var session: URLSession!
@@ -222,8 +327,11 @@ private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
222327
self.delegateQueue = OperationQueue()
223328
self.delegateQueue.name = "org.swift.swiftpm.urlsession-http-client-download-delegate"
224329
self.delegateQueue.maxConcurrentOperationCount = 1
225-
super.init()
226-
self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: self.delegateQueue)
330+
self.session = URLSession(configuration: configuration, delegate: WeakDownloadTaskManager(self), delegateQueue: self.delegateQueue)
331+
}
332+
333+
deinit {
334+
session.finishTasksAndInvalidate()
227335
}
228336

229337
func makeTask(
@@ -238,6 +346,7 @@ private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
238346
task: task,
239347
fileSystem: fileSystem,
240348
destination: destination,
349+
downloadTaskManager: self,
241350
progressHandler: progress,
242351
completionHandler: completion
243352
)
@@ -314,21 +423,28 @@ private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
314423
let task: URLSessionDownloadTask
315424
let fileSystem: FileSystem
316425
let destination: AbsolutePath
317-
let completionHandler: LegacyHTTPClient.CompletionHandler
426+
/// A strong reference to keep the `DownloadTaskManager` alive so it can handle the callbacks from the
427+
/// `URLSession`.
428+
///
429+
/// See comment on `WeakDownloadTaskManager`.
430+
private let downloadTaskManager: DownloadTaskManager
318431
let progressHandler: LegacyHTTPClient.ProgressHandler?
432+
let completionHandler: LegacyHTTPClient.CompletionHandler
319433

320434
var moveFileError: Error?
321435

322436
init(
323437
task: URLSessionDownloadTask,
324438
fileSystem: FileSystem,
325439
destination: AbsolutePath,
440+
downloadTaskManager: DownloadTaskManager,
326441
progressHandler: LegacyHTTPClient.ProgressHandler?,
327442
completionHandler: @escaping LegacyHTTPClient.CompletionHandler
328443
) {
329444
self.task = task
330445
self.fileSystem = fileSystem
331446
self.destination = destination
447+
self.downloadTaskManager = downloadTaskManager
332448
self.progressHandler = progressHandler
333449
self.completionHandler = completionHandler
334450
}

0 commit comments

Comments
 (0)