Просмотр исходного кода

RequestAdapter and RequestRetrier protocols now allow Requests to be retried.

Christian Noon 9 лет назад
Родитель
Сommit
5a2bea803c

+ 51 - 1
Source/Request.swift

@@ -24,9 +24,54 @@
 
 import Foundation
 
+/// A type that can adapt a `URLRequest` in some manner.
+public protocol RequestAdapter {
+    /// Adapts the specified `URLRequest` in some manner and returns the result.
+    ///
+    /// - parameter urlRequest: The URL request to adapt.
+    ///
+    /// - returns: The adapted `URLRequest`.
+    func adapt(_ urlRequest: URLRequest) -> URLRequest
+}
+
+// MARK: -
+
+/// A closure executed when the `RequestRetrier` determines whether a `Request` should be retried or not.
+public typealias RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void
+
+/// A type that determines whether a request should be retried after being executed by the specified session manager
+/// and encountering an error.
+public protocol RequestRetrier {
+    /// Determines whether the `Request` should be retried by calling the `completion` closure.
+    ///
+    /// This operation is fully asychronous. Any amount of time can be taken to determine whether the request needs
+    /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly
+    /// cleaned up after.
+    ///
+    /// - parameter manager:    The session manager the request was executed on.
+    /// - parameter request:    The request that failed due to the encountered error.
+    /// - parameter error:      The error encountered when executing the request.
+    /// - parameter completion: The completion closure to be executed when retry decision has been determined.
+    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: RequestRetryCompletion)
+}
+
+// MARK: -
+
 /// Responsible for sending a request and receiving the response and associated data from the server, as well as
 /// managing its underlying `URLSessionTask`.
 open class Request {
+
+    // MARK: Helper Types
+
+    enum TaskConvertible {
+        case data(URLRequest)
+        case download(URLRequest)
+        case downloadResumeData(Data)
+        case uploadData(Data, URLRequest)
+        case uploadFile(URL, URLRequest)
+        case uploadStream(InputStream, URLRequest)
+    }
+
     /// A closure executed once a request has successfully completed in order to determine where to move the temporary
     /// file written to during the download process. The closure takes two arguments: the temporary file URL and the URL
     /// response, and returns a single argument: the file URL where the temporary file should be moved.
@@ -72,16 +117,21 @@ open class Request {
         return data
     }
 
+    let originalTask: TaskConvertible?
+
     var startTime: CFAbsoluteTime?
     var endTime: CFAbsoluteTime?
 
+    var validations: [() -> Void] = []
+
     private var taskDelegate: TaskDelegate
     private var taskDelegateLock = NSLock()
 
     // MARK: Lifecycle
 
-    init(session: URLSession, task: URLSessionTask) {
+    init(session: URLSession, task: URLSessionTask, originalTask: TaskConvertible?) {
         self.session = session
+        self.originalTask = originalTask
 
         switch task {
         case is URLSessionUploadTask:

+ 58 - 10
Source/SessionDelegate.swift

@@ -123,6 +123,9 @@ open class SessionDelegate: NSObject {
 
     // MARK: Properties
 
+    var retrier: RequestRetrier?
+    weak var sessionManager: SessionManager?
+
     private var requests: [Int: Request] = [:]
     private let lock = NSLock()
 
@@ -362,19 +365,64 @@ extension SessionDelegate: URLSessionTaskDelegate {
     /// - parameter task:    The task whose request finished transferring data.
     /// - parameter error:   If an error occurred, an error object indicating how the transfer failed, otherwise nil.
     open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
-        if let taskDidComplete = taskDidComplete {
-            taskDidComplete(session, task, error)
-        } else if let delegate = self[task]?.delegate {
-            delegate.urlSession(session, task: task, didCompleteWithError: error)
+        /// Executed after it is determined that the request is not going to be retried
+        let completeTask: (URLSession, URLSessionTask, Error?) -> Void = { [weak self] session, task, error in
+            guard let strongSelf = self else { return }
+
+            if let taskDidComplete = strongSelf.taskDidComplete {
+                taskDidComplete(session, task, error)
+            } else if let delegate = strongSelf[task]?.delegate {
+                delegate.urlSession(session, task: task, didCompleteWithError: error)
+            }
+
+            NotificationCenter.default.post(
+                name: Notification.Name.Task.DidComplete,
+                object: strongSelf,
+                userInfo: [Notification.Key.Task: task]
+            )
+
+            strongSelf[task] = nil
+        }
+
+        guard let request = self[task], let sessionManager = sessionManager else {
+            completeTask(session, task, error)
+            return
         }
 
-        NotificationCenter.default.post(
-            name: Notification.Name.Task.DidComplete,
-            object: self,
-            userInfo: [Notification.Key.Task: task]
-        )
+        // Run all validations on the request before checking if an error occurred
+        request.validations.forEach { $0() }
 
-        self[task] = nil
+        // Determine whether an error has occurred
+        var error: Error? = error
+
+        if let taskDelegate = self[task]?.delegate, taskDelegate.error != nil {
+            error = taskDelegate.error
+        }
+
+        /// If an error occurred and the retrier is set, asynchronously ask the retrier if the request
+        /// should be retried. Otherwise, complete the task by notifying the task delegate.
+        if let retrier = retrier, let error = error {
+            retrier.should(sessionManager, retry: request, with: error) { [weak self] shouldRetry, delay in
+                if shouldRetry {
+                    DispatchQueue.utility.after(delay) { [weak self] in
+                        guard let strongSelf = self else { return }
+
+                        let retrySucceeded = strongSelf.sessionManager?.retry(request) ?? false
+
+                        if retrySucceeded {
+                            strongSelf[request.task] = request
+                            return
+                        } else {
+                            completeTask(session, task, error)
+                        }
+                    }
+                } else {
+                    completeTask(session, task, error)
+                }
+            }
+        } else {
+            completeTask(session, task, error)
+        }
     }
 }
 

+ 103 - 40
Source/SessionManager.swift

@@ -44,12 +44,32 @@ open class SessionManager {
     private enum Downloadable {
         case request(URLRequest)
         case resumeData(Data)
+
+        var task: Request.TaskConvertible {
+            switch self {
+            case let .request(urlRequest):
+                return .download(urlRequest)
+            case let .resumeData(resumeData):
+                return .downloadResumeData(resumeData)
+            }
+        }
     }
 
     private enum Uploadable {
         case data(Data, URLRequest)
         case file(URL, URLRequest)
         case stream(InputStream, URLRequest)
+
+        var task: Request.TaskConvertible {
+            switch self {
+            case let .data(data, urlRequest):
+                return .uploadData(data, urlRequest)
+            case let .file(url, urlRequest):
+                return .uploadFile(url, urlRequest)
+            case let .stream(stream, urlRequest):
+                return .uploadStream(stream, urlRequest)
+            }
+        }
     }
 
 #if !os(watchOS)
@@ -139,6 +159,15 @@ open class SessionManager {
     /// Whether to start requests immediately after being constructed. `true` by default.
     open var startRequestsImmediately: Bool = true
 
+    /// The request adapter called each time a new request is created.
+    open var adapter: RequestAdapter?
+
+    /// The request retrier called each time a request encounters an error to determine whether to retry the request.
+    open var retrier: RequestRetrier? {
+        get { return delegate.retrier }
+        set { delegate.retrier = newValue }
+    }
+
     /// The background completion handler closure provided by the UIApplicationDelegate
     /// `application:handleEventsForBackgroundURLSession:completionHandler:` method. By setting the background
     /// completion handler, the SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` closure implementation
@@ -199,6 +228,8 @@ open class SessionManager {
     private func commonInit(serverTrustPolicyManager: ServerTrustPolicyManager?) {
         session.serverTrustPolicyManager = serverTrustPolicyManager
 
+        delegate.sessionManager = self
+
         delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
             guard let strongSelf = self else { return }
             DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
@@ -244,10 +275,13 @@ open class SessionManager {
     ///
     /// - returns: The created data `Request`.
     open func request(_ urlRequest: URLRequestConvertible) -> Request {
+        let originalRequest = urlRequest.urlRequest
+        let adaptedRequest = adapt(originalRequest)
+
         var dataTask: URLSessionDataTask!
-        queue.sync { dataTask = self.session.dataTask(with: urlRequest.urlRequest) }
+        queue.sync { dataTask = self.session.dataTask(with: adaptedRequest) }
 
-        let request = Request(session: session, task: dataTask)
+        let request = Request(session: session, task: dataTask, originalTask: .data(originalRequest))
         delegate[request.delegate.task] = request
 
         if startRequestsImmediately {
@@ -328,25 +362,18 @@ open class SessionManager {
 
     // MARK: Private - Download Implementation
 
-    private func download(
-        _ downloadable: Downloadable,
-        to destination: Request.DownloadFileDestination)
-        -> Request
-    {
+    private func download(_ downloadable: Downloadable, to destination: Request.DownloadFileDestination) -> Request {
         var downloadTask: URLSessionDownloadTask!
 
         switch downloadable {
-        case .request(let request):
-            queue.sync {
-                downloadTask = self.session.downloadTask(with: request)
-            }
-        case .resumeData(let resumeData):
-            queue.sync {
-                downloadTask = self.session.downloadTask(withResumeData: resumeData)
-            }
+        case let .request(urlRequest):
+            let urlRequest = adapt(urlRequest)
+            queue.sync { downloadTask = self.session.downloadTask(with: urlRequest) }
+        case let .resumeData(resumeData):
+            queue.sync { downloadTask = self.session.downloadTask(withResumeData: resumeData) }
         }
 
-        let request = Request(session: session, task: downloadTask)
+        let request = Request(session: session, task: downloadTask, originalTask: downloadable.task)
 
         if let downloadDelegate = request.delegate as? DownloadTaskDelegate {
             downloadDelegate.downloadTaskDidFinishDownloadingToURL = { session, downloadTask, URL in
@@ -606,28 +633,22 @@ open class SessionManager {
         var HTTPBodyStream: InputStream?
 
         switch uploadable {
-        case .data(let data, let request):
-            queue.sync {
-                uploadTask = self.session.uploadTask(with: request, from: data)
-            }
-        case .file(let fileURL, let request):
-            queue.sync {
-                uploadTask = self.session.uploadTask(with: request, fromFile: fileURL)
-            }
-        case .stream(let stream, let request):
-            queue.sync {
-                uploadTask = self.session.uploadTask(withStreamedRequest: request)
-            }
-
+        case let .data(data, urlRequest):
+            let urlRequest = adapt(urlRequest)
+            queue.sync { uploadTask = self.session.uploadTask(with: urlRequest, from: data) }
+        case let .file(fileURL, urlRequest):
+            let urlRequest = adapt(urlRequest)
+            queue.sync { uploadTask = self.session.uploadTask(with: urlRequest, fromFile: fileURL) }
+        case let .stream(stream, urlRequest):
+            let urlRequest = adapt(urlRequest)
+            queue.sync { uploadTask = self.session.uploadTask(withStreamedRequest: urlRequest) }
             HTTPBodyStream = stream
         }
 
-        let request = Request(session: session, task: uploadTask)
+        let request = Request(session: session, task: uploadTask, originalTask: uploadable.task)
 
         if HTTPBodyStream != nil {
-            request.delegate.taskNeedNewBodyStream = { _, _ in
-                return HTTPBodyStream
-            }
+            request.delegate.taskNeedNewBodyStream = { _, _ in HTTPBodyStream }
         }
 
         delegate[request.delegate.task] = request
@@ -679,16 +700,12 @@ open class SessionManager {
 
         switch streamable {
         case .stream(let hostName, let port):
-            queue.sync {
-                streamTask = self.session.streamTask(withHostName: hostName, port: port)
-            }
+            queue.sync { streamTask = self.session.streamTask(withHostName: hostName, port: port) }
         case .netService(let netService):
-            queue.sync {
-                streamTask = self.session.streamTask(with: netService)
-            }
+            queue.sync { streamTask = self.session.streamTask(with: netService) }
         }
 
-        let request = Request(session: session, task: streamTask)
+        let request = Request(session: session, task: streamTask, originalTask: nil)
 
         delegate[request.delegate.task] = request
 
@@ -700,4 +717,50 @@ open class SessionManager {
     }
 
 #endif
+
+    // MARK: - Internal - Adapt Request
+
+    func adapt(_ urlRequest: URLRequest) -> URLRequest {
+        guard let adapter = adapter else { return urlRequest }
+        return adapter.adapt(urlRequest)
+    }
+
+    // MARK: - Internal - Retry Request
+
+    func retry(_ request: Request) -> Bool {
+        guard let originalTask = request.originalTask else { return false }
+
+        var task: URLSessionTask!
+
+        queue.sync {
+            switch originalTask {
+            case let .data(urlRequest):
+                let urlRequest = adapt(urlRequest)
+                task = self.session.dataTask(with: urlRequest)
+            case let .download(urlRequest):
+                let urlRequest = adapt(urlRequest)
+                task = self.session.downloadTask(with: urlRequest)
+            case let .downloadResumeData(resumeData):
+                task = self.session.downloadTask(withResumeData: resumeData)
+            case let .uploadData(data, urlRequest):
+                let urlRequest = adapt(urlRequest)
+                task = self.session.uploadTask(with: urlRequest, from: data)
+            case let .uploadFile(url, urlRequest):
+                let urlRequest = adapt(urlRequest)
+                task = self.session.uploadTask(with: urlRequest, fromFile: url)
+            case let .uploadStream(_, urlRequest):
+                let urlRequest = adapt(urlRequest)
+                task = self.session.uploadTask(withStreamedRequest: urlRequest)
+            }
+        }
+
+        request.delegate.task = task // resets all task delegate data
+
+        request.startTime = CFAbsoluteTimeGetCurrent()
+        request.endTime = nil
+
+        task.resume()
+
+        return true
+    }
 }

+ 25 - 1
Source/TaskDelegate.swift

@@ -33,7 +33,10 @@ open class TaskDelegate: NSObject {
     /// The serial operation queue used to execute all operations after the task completes.
     open let queue: OperationQueue
 
-    var task: URLSessionTask
+    var task: URLSessionTask {
+        didSet { reset() }
+    }
+
     let progress: Progress
 
     var data: Data? { return nil }
@@ -58,6 +61,11 @@ open class TaskDelegate: NSObject {
         }()
     }
 
+    func reset() {
+        error = nil
+        initialResponseTime = nil
+    }
+
     // MARK: URLSessionTaskDelegate
 
     var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
@@ -189,6 +197,14 @@ class DataTaskDelegate: TaskDelegate, URLSessionDataDelegate {
         super.init(task: task)
     }
 
+    override func reset() {
+        super.reset()
+
+        totalBytesReceived = 0
+        mutableData = Data()
+        expectedContentLength = nil
+    }
+
     // MARK: URLSessionDataDelegate
 
     var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
@@ -276,6 +292,14 @@ class DownloadTaskDelegate: TaskDelegate, URLSessionDownloadDelegate {
     var resumeData: Data?
     override var data: Data? { return resumeData }
 
+
+    // MARK: Lifecycle
+
+    override func reset() {
+        super.reset()
+        resumeData = nil
+    }
+
     // MARK: URLSessionDownloadDelegate
 
     var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> URL)?

+ 3 - 1
Source/Validation.swift

@@ -47,7 +47,7 @@ extension Request {
     /// - returns: The request.
     @discardableResult
     public func validate(_ validation: Validation) -> Self {
-        delegate.queue.addOperation {
+        let validationExecution: () -> Void = {
             if
                 let response = self.response,
                 self.delegate.error == nil,
@@ -57,6 +57,8 @@ extension Request {
             }
         }
 
+        validations.append(validationExecution)
+
         return self
     }
 

+ 3 - 4
Tests/ValidationTests.swift

@@ -237,13 +237,12 @@ class ContentTypeValidationTestCase: BaseTestCase {
         // Given
         class MockManager: SessionManager {
             override func request(_ urlRequest: URLRequestConvertible) -> Request {
+                let urlRequest = urlRequest.urlRequest
                 var dataTask: URLSessionDataTask!
 
-                queue.sync {
-                    dataTask = self.session.dataTask(with: urlRequest.urlRequest)
-                }
+                queue.sync { dataTask = self.session.dataTask(with: urlRequest) }
 
-                let request = MockRequest(session: session, task: dataTask)
+                let request = MockRequest(session: session, task: dataTask, originalTask: .data(urlRequest))
                 delegate[request.delegate.task] = request
 
                 if startRequestsImmediately {