Skip to content

Commit 46d9ed2

Browse files
committed
Fix NSURLSessionTask ‘state’.
1 parent fa86aaa commit 46d9ed2

File tree

2 files changed

+92
-52
lines changed

2 files changed

+92
-52
lines changed

Foundation/NSURLSession.swift

Lines changed: 79 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,9 @@ public class NSURLSessionTask : NSObject, NSCopying {
493493
if oldValue.isEasyHandlePaused && !internalState.isEasyHandlePaused {
494494
fatalError("Need to solve pausing receive.")
495495
}
496+
if case .taskCompleted = internalState {
497+
updateTaskState()
498+
}
496499
}
497500
}
498501
private let workQueue: dispatch_queue_t
@@ -559,9 +562,7 @@ public class NSURLSessionTask : NSObject, NSCopying {
559562
dispatch_sync(taskAttributesIsolation) { r = self._currentRequest }
560563
return r
561564
}
562-
set {
563-
dispatch_barrier_async(taskAttributesIsolation) { self._currentRequest = newValue }
564-
}
565+
set { dispatch_barrier_async(taskAttributesIsolation) { self._currentRequest = newValue } }
565566
}
566567
private var _currentRequest: NSURLRequest? = nil
567568
/*@NSCopying*/ public private(set) var response: NSURLResponse? {
@@ -570,9 +571,7 @@ public class NSURLSessionTask : NSObject, NSCopying {
570571
dispatch_sync(taskAttributesIsolation) { r = self._response }
571572
return r
572573
}
573-
set {
574-
dispatch_barrier_async(taskAttributesIsolation) { self._response = newValue }
575-
}
574+
set { dispatch_barrier_async(taskAttributesIsolation) { self._response = newValue } }
576575
}
577576
private var _response: NSURLResponse? = nil
578577

@@ -588,9 +587,7 @@ public class NSURLSessionTask : NSObject, NSCopying {
588587
dispatch_sync(taskAttributesIsolation) { r = self._countOfBytesReceived }
589588
return r
590589
}
591-
set {
592-
dispatch_barrier_async(taskAttributesIsolation) { self._countOfBytesReceived = newValue }
593-
}
590+
set { dispatch_barrier_async(taskAttributesIsolation) { self._countOfBytesReceived = newValue } }
594591
}
595592
private var _countOfBytesReceived: Int64 = 0
596593

@@ -601,9 +598,7 @@ public class NSURLSessionTask : NSObject, NSCopying {
601598
dispatch_sync(taskAttributesIsolation) { r = self._countOfBytesSent }
602599
return r
603600
}
604-
set {
605-
dispatch_barrier_async(taskAttributesIsolation) { self._countOfBytesSent = newValue }
606-
}
601+
set { dispatch_barrier_async(taskAttributesIsolation) { self._countOfBytesSent = newValue } }
607602
}
608603
private var _countOfBytesSent: Int64 = 0
609604

@@ -629,10 +624,14 @@ public class NSURLSessionTask : NSObject, NSCopying {
629624
* The current state of the task within the session.
630625
*/
631626
public var state: NSURLSessionTaskState {
632-
//TODO: Make this thread-safe
633-
guard suspendCount == 0 else { return .Suspended }
634-
NSUnimplemented()
627+
get {
628+
var r: NSURLSessionTaskState = .Suspended
629+
dispatch_sync(taskAttributesIsolation) { r = self._state }
630+
return r
631+
}
632+
set { dispatch_barrier_async(taskAttributesIsolation) { self._state = newValue } }
635633
}
634+
private var _state: NSURLSessionTaskState = .Suspended
636635

637636
/*
638637
* The error, if any, delivered via -URLSession:task:didCompleteWithError:
@@ -659,25 +658,38 @@ public class NSURLSessionTask : NSObject, NSCopying {
659658
//
660659
// TODO: It may be worth looking into starting over a task that gets
661660
// resumed. The Darwin Foundation documentation states that that's what
662-
// it down for anything but download tasks.
663-
//
664-
// TODO: is `suspend` / `resume` supposed to be thread safe? If so, we'd
665-
// need to use atomic increment / decrement of the suspend count.
666-
dispatch_async(workQueue) {
661+
// it does for anything but download tasks.
662+
663+
// We perform the increment and call to `updateTaskState()`
664+
// synchronous, to make sure the `state` is updated when this method
665+
// returns, but the actual suspend will be done asynchronous to avoid
666+
// dead-locks.
667+
dispatch_sync(workQueue) {
667668
self.suspendCount += 1
668-
guard self.suspendCount == 1 else { return }
669-
self.performSuspend()
669+
guard self.suspendCount < Int.max else { fatalError("Task suspended too many times \(Int.max).") }
670+
self.updateTaskState()
671+
672+
if self.suspendCount == 1 {
673+
dispatch_async(self.workQueue) {
674+
self.performSuspend()
675+
}
676+
}
670677
}
671678
}
672679
/// Resume the task.
673680
///
674681
/// - SeeAlso: `suspend()`
675682
public func resume() {
676-
dispatch_async(workQueue) {
683+
dispatch_sync(workQueue) {
677684
self.suspendCount -= 1
678685
guard 0 <= self.suspendCount else { fatalError("Resuming a task that's not suspended. Calls to resume() / suspend() need to be matched.") }
679-
guard self.suspendCount == 0 else { return }
680-
self.performResume()
686+
self.updateTaskState()
687+
688+
if self.suspendCount == 0 {
689+
dispatch_async(self.workQueue) {
690+
self.performResume()
691+
}
692+
}
681693
}
682694
}
683695

@@ -709,6 +721,27 @@ public class NSURLSessionTask : NSObject, NSCopying {
709721
private var _priority: Float = NSURLSessionTaskPriorityDefault
710722
}
711723

724+
private extension NSURLSessionTask {
725+
/// The calls to `suspend` can be nested. This one is only called when the
726+
/// task is not suspended and needs to go into suspended state.
727+
func performSuspend() {
728+
if case .transferInProgress(let transferState) = internalState {
729+
internalState = .transferReady(transferState)
730+
}
731+
}
732+
/// The calls to `resume` can be nested. This one is only called when the
733+
/// task is suspended and needs to go out of suspended state.
734+
func performResume() {
735+
if case .initial = internalState {
736+
guard let r = originalRequest else { fatalError("Task has no original request.") }
737+
startNewTransfer(r)
738+
}
739+
if case .transferReady(let transferState) = internalState {
740+
internalState = .transferInProgress(transferState)
741+
}
742+
}
743+
}
744+
712745
internal extension NSURLSessionTask {
713746
/// The is independent of the public `state: NSURLSessionTaskState`.
714747
enum InternalState {
@@ -744,28 +777,11 @@ internal extension NSURLSessionTask {
744777
case waitingForResponseCompletionHandler(TransferState)
745778
/// The task is completed
746779
///
747-
/// Contrast this with `.transferComplted`.
780+
/// Contrast this with `.transferCompleted`.
748781
case taskCompleted
749782
}
750-
/// The calls to `suspend` can be nested. This one is only called when the
751-
/// task is not suspended and needs to go into suspended state.
752-
private func performSuspend() {
753-
if case .transferInProgress(let transferState) = internalState {
754-
internalState = .transferReady(transferState)
755-
}
756-
}
757-
/// The calls to `resume` can be nested. This one is only called when the
758-
/// task is suspended and needs to go out of suspended state.
759-
private func performResume() {
760-
if case .initial = internalState {
761-
guard let r = originalRequest else { fatalError("Task has no original request.") }
762-
startNewTransfer(r)
763-
}
764-
if case .transferReady(let transferState) = internalState {
765-
internalState = .transferInProgress(transferState)
766-
}
767-
}
768783
}
784+
769785
private extension NSURLSessionTask.InternalState {
770786
var isEasyHandleAddedToMultiHandle: Bool {
771787
switch self {
@@ -793,6 +809,25 @@ private extension NSURLSessionTask.InternalState {
793809
}
794810
}
795811

812+
internal extension NSURLSessionTask {
813+
/// Updates the (public) state based on private / internal state.
814+
///
815+
/// - Note: This must be called on the `workQueue`.
816+
private func updateTaskState() {
817+
func calculateState() -> NSURLSessionTaskState {
818+
if case .taskCompleted = internalState {
819+
return .Completed
820+
}
821+
if suspendCount == 0 {
822+
return .Running
823+
} else {
824+
return .Suspended
825+
}
826+
}
827+
state = calculateState()
828+
}
829+
}
830+
796831
private extension NSURLSessionTask {
797832
enum Body {
798833
case none

TestFoundation/TestNSURLSession.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ extension TestNSURLSession {
208208
delegate.completionExpectation = expectationWithDescription("Task did complete")
209209
task.resume()
210210
waitForExpectationsWithTimeout(0.1, handler: nil)
211+
XCTAssertEqual(task.state, NSURLSessionTaskState.Completed)
211212

212213
guard let error = delegate.completionError else { XCTFail("URL: '\(urlString)'"); return }
213214
XCTAssertEqual(error.domain, NSURLErrorDomain, "URL: '\(urlString)'")
@@ -243,6 +244,7 @@ extension TestNSURLSession {
243244
delegate.completionExpectation = expectationWithDescription("Task did complete")
244245
task.resume()
245246
waitForExpectationsWithTimeout(1000, handler: nil)
247+
XCTAssertEqual(task.state, NSURLSessionTaskState.Completed)
246248

247249
guard let error = delegate.completionError else { XCTFail(); return }
248250
XCTAssertEqual(error.domain, NSURLErrorDomain)
@@ -279,10 +281,12 @@ extension TestNSURLSession {
279281
let task = sut.dataTaskWithURL(originalURL)
280282

281283
delegate.redirectExpectation = expectationWithDescription("Redirect")
284+
XCTAssertEqual(task.state, NSURLSessionTaskState.Suspended)
282285
task.resume()
283286

284287
waitForExpectationsWithTimeout(1.0, handler: nil)
285-
288+
XCTAssertEqual(task.state, NSURLSessionTaskState.Running)
289+
286290
do {
287291
XCTAssertEqual(delegate.allResponses.count, 1, "Should only have received the redirect response.")
288292
guard let r = delegate.redirect else { XCTFail("No redirect."); return }
@@ -302,6 +306,7 @@ extension TestNSURLSession {
302306
r.completionHandler(r.newRequest)
303307
}
304308
waitForExpectationsWithTimeout(0.1, handler: nil)
309+
XCTAssertEqual(task.state, NSURLSessionTaskState.Completed)
305310

306311
do {
307312
XCTAssertEqual(delegate.allResponses.count, 2, "Should have received the redirect response and the final.")
@@ -360,8 +365,11 @@ extension TestNSURLSession {
360365
completionExpectation.fulfill()
361366
})
362367

368+
XCTAssertEqual(task.state, NSURLSessionTaskState.Suspended)
363369
task.resume()
370+
XCTAssertEqual(task.state, NSURLSessionTaskState.Running)
364371
waitForExpectationsWithTimeout(0.1, handler: nil)
372+
XCTAssertEqual(task.state, NSURLSessionTaskState.Completed)
365373
}
366374
}
367375
}
@@ -371,13 +379,6 @@ extension TestNSURLSession {
371379
// NSURLSessionUploadTask
372380
// Requests with a body
373381
//
374-
// public func uploadTaskWithRequest(request: NSURLRequest, fromFile fileURL: NSURL) -> NSURLSessionUploadTask
375-
// public func uploadTaskWithRequest(request: NSURLRequest, fromData bodyData: NSData) -> NSURLSessionUploadTask
376-
// public func uploadTaskWithStreamedRequest(request: NSURLRequest) -> NSURLSessionUploadTask
377-
//
378-
// public func uploadTaskWithRequest(request: NSURLRequest, fromFile fileURL: NSURL, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionUploadTask
379-
// public func uploadTaskWithRequest(request: NSURLRequest, fromData bodyData: NSData?, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionUploadTask
380-
//
381382

382383
/// Test a task with a completion handler performs a `POST` request.
383384
func test_functional_POST_fromData() {
@@ -543,6 +544,10 @@ extension TestNSURLSession {
543544
}
544545
}
545546
}
547+
548+
//TODO: Uploading data with completion handler
549+
// public func uploadTaskWithRequest(request: NSURLRequest, fromFile fileURL: NSURL, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionUploadTask
550+
// public func uploadTaskWithRequest(request: NSURLRequest, fromData bodyData: NSData?, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionUploadTask
546551
}
547552

548553
private extension HTTPResponse {

0 commit comments

Comments
 (0)