Skip to content

Commit

Permalink
fix: allow url async calls to run in parallel (#394)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cbaker6 authored Aug 29, 2022
1 parent e3e861d commit 15d1724
Show file tree
Hide file tree
Showing 10 changed files with 562 additions and 34 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ disabled_rules:
- type_body_length
- inclusive_language
- comment_spacing
- identifier_name
excluded: # paths to ignore during linting. Takes precedence over `included`.
- Tests/ParseSwiftTests/ParseEncoderTests
- DerivedData
Expand Down
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Parse-Swift Changelog

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

### 4.9.1
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.0...4.9.1)
__Fixes__
- 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).

### 4.9.0
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.8.0...4.9.0)

Expand Down
10 changes: 9 additions & 1 deletion Sources/ParseSwift/API/API+Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,18 @@ internal extension API {
case .success(let urlRequest):
if method == .POST || method == .PUT || method == .PATCH {
let task = URLSession.parse.uploadTask(withStreamedRequest: urlRequest)
ParseSwift.sessionDelegate.uploadDelegates[task] = uploadProgress
ParseSwift.sessionDelegate.streamDelegates[task] = stream
#if compiler(>=5.5.2) && canImport(_Concurrency)
Task {
await ParseSwift.sessionDelegate.delegates.updateUpload(task, callback: uploadProgress)
await ParseSwift.sessionDelegate.delegates.updateTask(task, queue: callbackQueue)
task.resume()
}
#else
ParseSwift.sessionDelegate.uploadDelegates[task] = uploadProgress
ParseSwift.sessionDelegate.taskCallbackQueues[task] = callbackQueue
task.resume()
#endif
return
}
case .failure(let error):
Expand Down
133 changes: 105 additions & 28 deletions Sources/ParseSwift/API/ParseURLSessionDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,56 @@ import Foundation
import FoundationNetworking
#endif

class ParseURLSessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate
class ParseURLSessionDelegate: NSObject
{
var callbackQueue: DispatchQueue
var authentication: ((URLAuthenticationChallenge,
(URLSession.AuthChallengeDisposition,
URLCredential?) -> Void) -> Void)?
var streamDelegates = [URLSessionTask: InputStream]()
#if compiler(>=5.5.2) && canImport(_Concurrency)
actor SessionDelegate: Sendable {
var downloadDelegates = [URLSessionDownloadTask: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)]()
var uploadDelegates = [URLSessionTask: ((URLSessionTask, Int64, Int64, Int64) -> Void)]()
var taskCallbackQueues = [URLSessionTask: DispatchQueue]()

func updateDownload(_ task: URLSessionDownloadTask,
callback: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?) {
downloadDelegates[task] = callback
}

func removeDownload(_ task: URLSessionDownloadTask) {
downloadDelegates.removeValue(forKey: task)
taskCallbackQueues.removeValue(forKey: task)
}

func updateUpload(_ task: URLSessionTask,
callback: ((URLSessionTask, Int64, Int64, Int64) -> Void)?) {
uploadDelegates[task] = callback
}

func removeUpload(_ task: URLSessionTask) {
uploadDelegates.removeValue(forKey: task)
taskCallbackQueues.removeValue(forKey: task)
}

func updateTask(_ task: URLSessionTask,
queue: DispatchQueue) {
taskCallbackQueues[task] = queue
}

func removeTask(_ task: URLSessionTask) {
taskCallbackQueues.removeValue(forKey: task)
}
}

var delegates = SessionDelegate()

#else
var downloadDelegates = [URLSessionDownloadTask: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)]()
var uploadDelegates = [URLSessionTask: ((URLSessionTask, Int64, Int64, Int64) -> Void)]()
var streamDelegates = [URLSessionTask: InputStream]()
var taskCallbackQueues = [URLSessionTask: DispatchQueue]()
#endif

init (callbackQueue: DispatchQueue,
authentication: ((URLAuthenticationChallenge,
Expand All @@ -30,7 +70,9 @@ class ParseURLSessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDeleg
self.authentication = authentication
super.init()
}
}

extension ParseURLSessionDelegate: URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition,
Expand All @@ -43,60 +85,95 @@ class ParseURLSessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDeleg
completionHandler(.performDefaultHandling, nil)
}
}
}

extension ParseURLSessionDelegate: URLSessionDataDelegate {
func urlSession(_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64) {
if let callBack = uploadDelegates[task],
#if compiler(>=5.5.2) && canImport(_Concurrency)
Task {
if let callback = await delegates.uploadDelegates[task],
let queue = await delegates.taskCallbackQueues[task] {
queue.async {
callback(task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
}
}
}
#else
if let callback = uploadDelegates[task],
let queue = taskCallbackQueues[task] {

queue.async {
callBack(task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
callback(task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
}
}
#endif
}

if totalBytesSent == totalBytesExpectedToSend {
self.uploadDelegates.removeValue(forKey: task)
}
func urlSession(_ session: URLSession,
task: URLSessionTask,
needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
if let stream = streamDelegates[task] {
completionHandler(stream)
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
streamDelegates.removeValue(forKey: task)
#if compiler(>=5.5.2) && canImport(_Concurrency)
Task {
await delegates.removeUpload(task)
if let downloadTask = task as? URLSessionDownloadTask {
await delegates.removeDownload(downloadTask)
}
}
#else
uploadDelegates.removeValue(forKey: task)
taskCallbackQueues.removeValue(forKey: task)
if let downloadTask = task as? URLSessionDownloadTask {
downloadDelegates.removeValue(forKey: downloadTask)
}
#endif
}
}

extension ParseURLSessionDelegate: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {

if let callBack = downloadDelegates[downloadTask],
#if compiler(>=5.5.2) && canImport(_Concurrency)
Task {
if let callback = await delegates.downloadDelegates[downloadTask],
let queue = await delegates.taskCallbackQueues[downloadTask] {
queue.async {
callback(downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
}
}
}
#else
if let callback = downloadDelegates[downloadTask],
let queue = taskCallbackQueues[downloadTask] {
queue.async {
callBack(downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
if totalBytesWritten == totalBytesExpectedToWrite {
self.downloadDelegates.removeValue(forKey: downloadTask)
}
callback(downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
}
}
#endif
}

func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
#if compiler(>=5.5.2) && canImport(_Concurrency)
Task {
await delegates.removeDownload(downloadTask)
}
#else
downloadDelegates.removeValue(forKey: downloadTask)
taskCallbackQueues.removeValue(forKey: downloadTask)
}

func urlSession(_ session: URLSession,
task: URLSessionTask,
needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
if let stream = streamDelegates[task] {
completionHandler(stream)
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
uploadDelegates.removeValue(forKey: task)
streamDelegates.removeValue(forKey: task)
taskCallbackQueues.removeValue(forKey: task)
#endif
}
}
46 changes: 44 additions & 2 deletions Sources/ParseSwift/Extensions/URLSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import FoundationNetworking
#endif

internal extension URLSession {
static let parse: URLSession = {
#if !os(Linux) && !os(Android) && !os(Windows)
static var parse = URLSession.shared
#else
static var parse: URLSession = /* URLSession.shared */ {
if !ParseSwift.configuration.isTestingSDK {
let configuration = URLSessionConfiguration.default
configuration.urlCache = URLCache.parse
Expand All @@ -25,9 +28,32 @@ internal extension URLSession {
} else {
let session = URLSession.shared
session.configuration.urlCache = URLCache.parse
return URLSession.shared
session.configuration.requestCachePolicy = ParseSwift.configuration.requestCachePolicy
session.configuration.httpAdditionalHeaders = ParseSwift.configuration.httpAdditionalHeaders
return session
}
}()
#endif

static func updateParseURLSession() {
#if !os(Linux) && !os(Android) && !os(Windows)
if !ParseSwift.configuration.isTestingSDK {
let configuration = URLSessionConfiguration.default
configuration.urlCache = URLCache.parse
configuration.requestCachePolicy = ParseSwift.configuration.requestCachePolicy
configuration.httpAdditionalHeaders = ParseSwift.configuration.httpAdditionalHeaders
Self.parse = URLSession(configuration: configuration,
delegate: ParseSwift.sessionDelegate,
delegateQueue: nil)
} else {
let session = URLSession.shared
session.configuration.urlCache = URLCache.parse
session.configuration.requestCachePolicy = ParseSwift.configuration.requestCachePolicy
session.configuration.httpAdditionalHeaders = ParseSwift.configuration.httpAdditionalHeaders
Self.parse = session
}
#endif
}

static func reconnectInterval(_ maxExponent: Int) -> Int {
let min = NSDecimalNumber(decimal: Swift.min(30, pow(2, maxExponent) - 1))
Expand Down Expand Up @@ -220,9 +246,17 @@ internal extension URLSession {
completion(.failure(ParseError(code: .unknownError, message: "data and file both cannot be nil")))
}
if let task = task {
#if compiler(>=5.5.2) && canImport(_Concurrency)
Task {
await ParseSwift.sessionDelegate.delegates.updateUpload(task, callback: progress)
await ParseSwift.sessionDelegate.delegates.updateTask(task, queue: notificationQueue)
task.resume()
}
#else
ParseSwift.sessionDelegate.uploadDelegates[task] = progress
ParseSwift.sessionDelegate.taskCallbackQueues[task] = notificationQueue
task.resume()
#endif
}
}

Expand Down Expand Up @@ -255,9 +289,17 @@ internal extension URLSession {
}
completion(result)
}
#if compiler(>=5.5.2) && canImport(_Concurrency)
Task {
await ParseSwift.sessionDelegate.delegates.updateDownload(task, callback: progress)
await ParseSwift.sessionDelegate.delegates.updateTask(task, queue: notificationQueue)
task.resume()
}
#else
ParseSwift.sessionDelegate.downloadDelegates[task] = progress
ParseSwift.sessionDelegate.taskCallbackQueues[task] = notificationQueue
task.resume()
#endif
}

func downloadTask<U>(
Expand Down
2 changes: 2 additions & 0 deletions Sources/ParseSwift/Parse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ public struct ParseSwift {
Self.configuration = configuration
Self.sessionDelegate = ParseURLSessionDelegate(callbackQueue: .main,
authentication: configuration.authentication)
URLSession.updateParseURLSession()
deleteKeychainIfNeeded()

#if !os(Linux) && !os(Android) && !os(Windows)
Expand Down Expand Up @@ -626,6 +627,7 @@ public struct ParseSwift {
URLCredential?) -> Void) -> Void)?) {
Self.sessionDelegate = ParseURLSessionDelegate(callbackQueue: .main,
authentication: authentication)
URLSession.updateParseURLSession()
}

#if !os(Linux) && !os(Android) && !os(Windows)
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/ParseConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation

enum ParseConstants {
static let sdk = "swift"
static let version = "4.9.0"
static let version = "4.9.1"
static let fileManagementDirectory = "parse/"
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
static let fileManagementLibraryDirectory = "Library/"
Expand Down
Loading

0 comments on commit 15d1724

Please sign in to comment.