Background Transfer Session Recreation

Hi,


I'm building a framework that creates a URLSession with a backgroundTransfer configuration. We have settings that allow our callers to switch settings on our session.


When these settings change, we proceed to invalidate the session, wait for the delegate callback (session:didInvalidateWithError:) and proceed to recreate the session with the new settings within that callback.


I'm trying to write a unit test to test this code and I see that it's not working. I keep getting messages in the console that a background session already exists with that identifier. I've tried nilling out my reference to the session, dispatch asyncing and dispatch_after for the recreation calls.


Is it just not possible to change the settings of a background transfer session's URLSessionConfiguration?


Thanks

Replies

The technique you’ve described should work. And, in fact, it’s working for me. I created a simple test project from the Single View Application template, configured the code as shown below, wired up a Test button to

testAction(_:)
, and this is what gets printed:
… set up session
… will invalidate session
… invalidate, no error
… set up session

The first line is emitted on launch and the remaining lines when I tap the Test button.

This is Xcode 8 on 10.11.6 targeting iOS 10.0.2.

Please try this test and let me know what you see.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
class ViewController: UIViewController, URLSessionDelegate {

    var session: URLSession? = nil

    func setupSession() {
        NSLog("set up session")
        let config = URLSessionConfiguration.background(withIdentifier: "com.example.apple-samplecode.xxsi")
        self.session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupSession()
    }

    @IBAction func testAction(_ sender: AnyObject) {
        if let session = self.session {
            NSLog("will invalidate session")
            self.session = nil
            session.invalidateAndCancel()
        } else {
            NSLog("no session to invalidate")
        }
    }

    func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
        if let error = error as? NSError {
            NSLog("invalidate, error %@ / %d", error.domain, error.code)
        } else {
            NSLog("invalidate, no error")
        }
        self.setupSession()
    }
}

Hi Quinn,


Thanks for the quick reply!


I've been meaning to post my code so you can see what I'm doing:


func recreateSession(with configuration: URLSessionConfiguration, completionHandler: (() -> Void)?) {
        sessionDelegate.sessionDidBecomeInvalidWithError = { [weak self] session, error in
            guard let strongSelf = self else { return }

            strongSelf.sessionDelegate = SessionDelegate()
            strongSelf.session = URLSession(configuration: configuration, delegate: strongSelf.sessionDelegate, delegateQueue: strongSelf.delegateQueue)

            completionHandler?()
        }

        session.finishTasksAndInvalidate()
}


func updateAllowOverCellularMode(to allowOverCellularMode: Bool, completionHandler: (() -> Void)?) {
        let configuration = sessionConfiguration
        configuration.allowsCellularAccess = allowOverCellularMode
        self.recreateSession(with: configuration, completionHandler: completionHandler)
}


I'm testing this code like so:


fileprivate let sessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "identifier")


override func setUp() {
        super.setUp()
        sessionManager = DefaultBackgroundTransferURLSessionManager(configuration: sessionConfiguration, maxRequestConcurrencyCount: maxRequestConcurrencyCount, qualityOfService: qualityOfService)
    }

override func tearDown() {
     super.tearDown()
     sessionManager = nil
}

func testUpdateAllowOverCellularMode() {
        guard let sessionManager = sessionManager else { XCTFail(); return }

          let testExpectation = expectation(description: "Session Recreation Expectation")
             sessionManager.updateAllowOverCellularMode(to: true) {
                 XCTAssertTrue(sessionManager.allowOverCellularMode)
                 XCTAssertTrue(sessionManager.sessionConfiguration.allowsCellularAccess)
                 testExpectation.fulfill()
        }
        waitForExpectations(timeout: 5) { error in
            XCTAssertNil(error, "We encountered an error?")
        }
    }


This test only passes sometimes....sometimes it hangs and I get this printed in the console:


2016-10-13 17:24:12.352 xctest[66734:54590554] A background URLSession with identifier identifier already exists!

Alas this isn’t really enough to go on. To start, you’re using a bunch of stuff (for example,

SessionDelegate
) that you don’t explain.

Two suggestions:

  • Check that your delegate queue is a serial queue. I’ve seen some very odd things happen if you use a concurrent queue for NSURLSession.

  • Put my code into an XCTest to see if the issue is tied to the XCTest environment.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"