Skip to content

Commit 15d1724

Browse files
authored
fix: allow url async calls to run in parallel (#394)
* fix: allow url async calls to run in parallel * revert * add tests * add changelog * improve parse url session * don't test on linux * fix build on old swift * revert url session for linux tests * test not running user tests on linux * fix test on older linux * fix session for tests * fix test session for linux * test old setup for linux * working url session for all OS's * add delegate tests * add old swift tests * nit * extend time * make delegates sendable * update test suite
1 parent e3e861d commit 15d1724

10 files changed

+562
-34
lines changed

.swiftlint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ disabled_rules:
55
- type_body_length
66
- inclusive_language
77
- comment_spacing
8+
- identifier_name
89
excluded: # paths to ignore during linting. Takes precedence over `included`.
910
- Tests/ParseSwiftTests/ParseEncoderTests
1011
- DerivedData

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
# Parse-Swift Changelog
22

33
### main
4-
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.0...main)
4+
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.1...main)
55
* _Contributing to this repo? Add info about your change here to be included in the next release_
66

7+
### 4.9.1
8+
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.0...4.9.1)
9+
__Fixes__
10+
- Corrects a memory leak where multiple Parse URLSessions can get created. Use an actor for the url session delegates to ensure thread safety when making async calls in parallel ([#394](https://github.com/parse-community/Parse-Swift/pull/394)), thanks to [Corey Baker](https://github.com/cbaker6).
11+
712
### 4.9.0
813
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.8.0...4.9.0)
914

Sources/ParseSwift/API/API+Command.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,18 @@ internal extension API {
6060
case .success(let urlRequest):
6161
if method == .POST || method == .PUT || method == .PATCH {
6262
let task = URLSession.parse.uploadTask(withStreamedRequest: urlRequest)
63-
ParseSwift.sessionDelegate.uploadDelegates[task] = uploadProgress
6463
ParseSwift.sessionDelegate.streamDelegates[task] = stream
64+
#if compiler(>=5.5.2) && canImport(_Concurrency)
65+
Task {
66+
await ParseSwift.sessionDelegate.delegates.updateUpload(task, callback: uploadProgress)
67+
await ParseSwift.sessionDelegate.delegates.updateTask(task, queue: callbackQueue)
68+
task.resume()
69+
}
70+
#else
71+
ParseSwift.sessionDelegate.uploadDelegates[task] = uploadProgress
6572
ParseSwift.sessionDelegate.taskCallbackQueues[task] = callbackQueue
6673
task.resume()
74+
#endif
6775
return
6876
}
6977
case .failure(let error):

Sources/ParseSwift/API/ParseURLSessionDelegate.swift

Lines changed: 105 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,56 @@ import Foundation
1111
import FoundationNetworking
1212
#endif
1313

14-
class ParseURLSessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate
14+
class ParseURLSessionDelegate: NSObject
1515
{
1616
var callbackQueue: DispatchQueue
1717
var authentication: ((URLAuthenticationChallenge,
1818
(URLSession.AuthChallengeDisposition,
1919
URLCredential?) -> Void) -> Void)?
20+
var streamDelegates = [URLSessionTask: InputStream]()
21+
#if compiler(>=5.5.2) && canImport(_Concurrency)
22+
actor SessionDelegate: Sendable {
23+
var downloadDelegates = [URLSessionDownloadTask: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)]()
24+
var uploadDelegates = [URLSessionTask: ((URLSessionTask, Int64, Int64, Int64) -> Void)]()
25+
var taskCallbackQueues = [URLSessionTask: DispatchQueue]()
26+
27+
func updateDownload(_ task: URLSessionDownloadTask,
28+
callback: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?) {
29+
downloadDelegates[task] = callback
30+
}
31+
32+
func removeDownload(_ task: URLSessionDownloadTask) {
33+
downloadDelegates.removeValue(forKey: task)
34+
taskCallbackQueues.removeValue(forKey: task)
35+
}
36+
37+
func updateUpload(_ task: URLSessionTask,
38+
callback: ((URLSessionTask, Int64, Int64, Int64) -> Void)?) {
39+
uploadDelegates[task] = callback
40+
}
41+
42+
func removeUpload(_ task: URLSessionTask) {
43+
uploadDelegates.removeValue(forKey: task)
44+
taskCallbackQueues.removeValue(forKey: task)
45+
}
46+
47+
func updateTask(_ task: URLSessionTask,
48+
queue: DispatchQueue) {
49+
taskCallbackQueues[task] = queue
50+
}
51+
52+
func removeTask(_ task: URLSessionTask) {
53+
taskCallbackQueues.removeValue(forKey: task)
54+
}
55+
}
56+
57+
var delegates = SessionDelegate()
58+
59+
#else
2060
var downloadDelegates = [URLSessionDownloadTask: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)]()
2161
var uploadDelegates = [URLSessionTask: ((URLSessionTask, Int64, Int64, Int64) -> Void)]()
22-
var streamDelegates = [URLSessionTask: InputStream]()
2362
var taskCallbackQueues = [URLSessionTask: DispatchQueue]()
63+
#endif
2464

2565
init (callbackQueue: DispatchQueue,
2666
authentication: ((URLAuthenticationChallenge,
@@ -30,7 +70,9 @@ class ParseURLSessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDeleg
3070
self.authentication = authentication
3171
super.init()
3272
}
73+
}
3374

75+
extension ParseURLSessionDelegate: URLSessionDelegate {
3476
func urlSession(_ session: URLSession,
3577
didReceive challenge: URLAuthenticationChallenge,
3678
completionHandler: @escaping (URLSession.AuthChallengeDisposition,
@@ -43,60 +85,95 @@ class ParseURLSessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDeleg
4385
completionHandler(.performDefaultHandling, nil)
4486
}
4587
}
88+
}
4689

90+
extension ParseURLSessionDelegate: URLSessionDataDelegate {
4791
func urlSession(_ session: URLSession,
4892
task: URLSessionTask,
4993
didSendBodyData bytesSent: Int64,
5094
totalBytesSent: Int64,
5195
totalBytesExpectedToSend: Int64) {
52-
if let callBack = uploadDelegates[task],
96+
#if compiler(>=5.5.2) && canImport(_Concurrency)
97+
Task {
98+
if let callback = await delegates.uploadDelegates[task],
99+
let queue = await delegates.taskCallbackQueues[task] {
100+
queue.async {
101+
callback(task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
102+
}
103+
}
104+
}
105+
#else
106+
if let callback = uploadDelegates[task],
53107
let queue = taskCallbackQueues[task] {
54-
55108
queue.async {
56-
callBack(task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
109+
callback(task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
110+
}
111+
}
112+
#endif
113+
}
57114

58-
if totalBytesSent == totalBytesExpectedToSend {
59-
self.uploadDelegates.removeValue(forKey: task)
60-
}
115+
func urlSession(_ session: URLSession,
116+
task: URLSessionTask,
117+
needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
118+
if let stream = streamDelegates[task] {
119+
completionHandler(stream)
120+
}
121+
}
122+
123+
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
124+
streamDelegates.removeValue(forKey: task)
125+
#if compiler(>=5.5.2) && canImport(_Concurrency)
126+
Task {
127+
await delegates.removeUpload(task)
128+
if let downloadTask = task as? URLSessionDownloadTask {
129+
await delegates.removeDownload(downloadTask)
61130
}
62131
}
132+
#else
133+
uploadDelegates.removeValue(forKey: task)
134+
taskCallbackQueues.removeValue(forKey: task)
135+
if let downloadTask = task as? URLSessionDownloadTask {
136+
downloadDelegates.removeValue(forKey: downloadTask)
137+
}
138+
#endif
63139
}
140+
}
64141

142+
extension ParseURLSessionDelegate: URLSessionDownloadDelegate {
65143
func urlSession(_ session: URLSession,
66144
downloadTask: URLSessionDownloadTask,
67145
didWriteData bytesWritten: Int64,
68146
totalBytesWritten: Int64,
69147
totalBytesExpectedToWrite: Int64) {
70-
71-
if let callBack = downloadDelegates[downloadTask],
148+
#if compiler(>=5.5.2) && canImport(_Concurrency)
149+
Task {
150+
if let callback = await delegates.downloadDelegates[downloadTask],
151+
let queue = await delegates.taskCallbackQueues[downloadTask] {
152+
queue.async {
153+
callback(downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
154+
}
155+
}
156+
}
157+
#else
158+
if let callback = downloadDelegates[downloadTask],
72159
let queue = taskCallbackQueues[downloadTask] {
73160
queue.async {
74-
callBack(downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
75-
if totalBytesWritten == totalBytesExpectedToWrite {
76-
self.downloadDelegates.removeValue(forKey: downloadTask)
77-
}
161+
callback(downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
78162
}
79163
}
164+
#endif
80165
}
81166

82167
func urlSession(_ session: URLSession,
83168
downloadTask: URLSessionDownloadTask,
84169
didFinishDownloadingTo location: URL) {
170+
#if compiler(>=5.5.2) && canImport(_Concurrency)
171+
Task {
172+
await delegates.removeDownload(downloadTask)
173+
}
174+
#else
85175
downloadDelegates.removeValue(forKey: downloadTask)
86176
taskCallbackQueues.removeValue(forKey: downloadTask)
87-
}
88-
89-
func urlSession(_ session: URLSession,
90-
task: URLSessionTask,
91-
needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
92-
if let stream = streamDelegates[task] {
93-
completionHandler(stream)
94-
}
95-
}
96-
97-
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
98-
uploadDelegates.removeValue(forKey: task)
99-
streamDelegates.removeValue(forKey: task)
100-
taskCallbackQueues.removeValue(forKey: task)
177+
#endif
101178
}
102179
}

Sources/ParseSwift/Extensions/URLSession.swift

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import FoundationNetworking
1313
#endif
1414

1515
internal extension URLSession {
16-
static let parse: URLSession = {
16+
#if !os(Linux) && !os(Android) && !os(Windows)
17+
static var parse = URLSession.shared
18+
#else
19+
static var parse: URLSession = /* URLSession.shared */ {
1720
if !ParseSwift.configuration.isTestingSDK {
1821
let configuration = URLSessionConfiguration.default
1922
configuration.urlCache = URLCache.parse
@@ -25,9 +28,32 @@ internal extension URLSession {
2528
} else {
2629
let session = URLSession.shared
2730
session.configuration.urlCache = URLCache.parse
28-
return URLSession.shared
31+
session.configuration.requestCachePolicy = ParseSwift.configuration.requestCachePolicy
32+
session.configuration.httpAdditionalHeaders = ParseSwift.configuration.httpAdditionalHeaders
33+
return session
2934
}
3035
}()
36+
#endif
37+
38+
static func updateParseURLSession() {
39+
#if !os(Linux) && !os(Android) && !os(Windows)
40+
if !ParseSwift.configuration.isTestingSDK {
41+
let configuration = URLSessionConfiguration.default
42+
configuration.urlCache = URLCache.parse
43+
configuration.requestCachePolicy = ParseSwift.configuration.requestCachePolicy
44+
configuration.httpAdditionalHeaders = ParseSwift.configuration.httpAdditionalHeaders
45+
Self.parse = URLSession(configuration: configuration,
46+
delegate: ParseSwift.sessionDelegate,
47+
delegateQueue: nil)
48+
} else {
49+
let session = URLSession.shared
50+
session.configuration.urlCache = URLCache.parse
51+
session.configuration.requestCachePolicy = ParseSwift.configuration.requestCachePolicy
52+
session.configuration.httpAdditionalHeaders = ParseSwift.configuration.httpAdditionalHeaders
53+
Self.parse = session
54+
}
55+
#endif
56+
}
3157

3258
static func reconnectInterval(_ maxExponent: Int) -> Int {
3359
let min = NSDecimalNumber(decimal: Swift.min(30, pow(2, maxExponent) - 1))
@@ -220,9 +246,17 @@ internal extension URLSession {
220246
completion(.failure(ParseError(code: .unknownError, message: "data and file both cannot be nil")))
221247
}
222248
if let task = task {
249+
#if compiler(>=5.5.2) && canImport(_Concurrency)
250+
Task {
251+
await ParseSwift.sessionDelegate.delegates.updateUpload(task, callback: progress)
252+
await ParseSwift.sessionDelegate.delegates.updateTask(task, queue: notificationQueue)
253+
task.resume()
254+
}
255+
#else
223256
ParseSwift.sessionDelegate.uploadDelegates[task] = progress
224257
ParseSwift.sessionDelegate.taskCallbackQueues[task] = notificationQueue
225258
task.resume()
259+
#endif
226260
}
227261
}
228262

@@ -255,9 +289,17 @@ internal extension URLSession {
255289
}
256290
completion(result)
257291
}
292+
#if compiler(>=5.5.2) && canImport(_Concurrency)
293+
Task {
294+
await ParseSwift.sessionDelegate.delegates.updateDownload(task, callback: progress)
295+
await ParseSwift.sessionDelegate.delegates.updateTask(task, queue: notificationQueue)
296+
task.resume()
297+
}
298+
#else
258299
ParseSwift.sessionDelegate.downloadDelegates[task] = progress
259300
ParseSwift.sessionDelegate.taskCallbackQueues[task] = notificationQueue
260301
task.resume()
302+
#endif
261303
}
262304

263305
func downloadTask<U>(

Sources/ParseSwift/Parse.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ public struct ParseSwift {
267267
Self.configuration = configuration
268268
Self.sessionDelegate = ParseURLSessionDelegate(callbackQueue: .main,
269269
authentication: configuration.authentication)
270+
URLSession.updateParseURLSession()
270271
deleteKeychainIfNeeded()
271272

272273
#if !os(Linux) && !os(Android) && !os(Windows)
@@ -626,6 +627,7 @@ public struct ParseSwift {
626627
URLCredential?) -> Void) -> Void)?) {
627628
Self.sessionDelegate = ParseURLSessionDelegate(callbackQueue: .main,
628629
authentication: authentication)
630+
URLSession.updateParseURLSession()
629631
}
630632

631633
#if !os(Linux) && !os(Android) && !os(Windows)

Sources/ParseSwift/ParseConstants.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010

1111
enum ParseConstants {
1212
static let sdk = "swift"
13-
static let version = "4.9.0"
13+
static let version = "4.9.1"
1414
static let fileManagementDirectory = "parse/"
1515
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
1616
static let fileManagementLibraryDirectory = "Library/"

0 commit comments

Comments
 (0)