Alk
Alk

Reputation: 5567

Uploading Data Using NSURLSession and Queue

I am designing a chat application and I have set up the following mechanism for users to upload messages. Basically, I push the messages onto a queue and upload them one after the other. When the queue is empty, I call finishedUploading which runs every second and reruns the task if there is anything in the queue.

var uploadQueue:[UploadMessage]?
let session = NSURLSession.sharedSession()
let lockQueue = dispatch_queue_create("com.dsdevelop.lockQueue", nil)

// RETURNS AMOUNT OF ITEMS STILL IN QUEUE 

func getRemainingActiveUploads() -> Int {
return (self.uploadQueue != nil) ? self.uploadQueue!.count : 0
}

//REMOVES MESSAGE FROM QUEUE ONCE UPLOADED

func removeMessageFromUploadQueue(messageToBeRemoved : UploadMessage) {
if (uploadQueue != nil) {
    dispatch_sync(lockQueue) {
        self.uploadQueue = self.uploadQueue?.filter({$0.date!.compare(messageToBeRemoved.date!) == NSComparisonResult.OrderedSame})
    }
}
}

var uploadTimer : NSTimer?

// CALLED ONLY WHEN UPLOADQUEUE IS EMPTY, RERUNS THE UPLOAD FUNCTION AFTER 1 SECOND
func finishedUploading() {
uploadTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(uploadAllLinks), userInfo: nil, repeats: false)
if (needToRefetch) {
    needToRefetch = false
    newMessageReceived()
}
}

func uploadAllLinks()
{
uploadTimer?.invalidate()
uploadTimer = nil
// suspending queue so they don't all finish before we can show it
session.delegateQueue.suspended = true
session.delegateQueue.maxConcurrentOperationCount = 1

let myUrl = NSURL(string: "http://****")

// create tasks
if (uploadQueue != nil) {
    if (uploadQueue?.count > 0) {
    for message in uploadQueue!
    {
        let request = NSMutableURLRequest(URL:myUrl!)
        request.HTTPMethod = "POST"
        request.timeoutInterval = 10
        request.HTTPShouldHandleCookies=false

        var postString = "sender=" + message.sender! 
        request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding);

        let dltask = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) in
            if data != nil
            {
                do {
                    let jsonArray = try NSJSONSerialization.JSONObjectWithData(data_fixed!, options:[])
                    dispatch_async(dispatch_get_main_queue(), {

                        if let errorToken = jsonArray["error"] as! Bool? {
                            if  !errorToken  {
                              self.uploadQueue = self.uploadQueue!.filter({$0.date!.compare(message.date!) != NSComparisonResult.OrderedSame})
                                            let remaining = self.getRemainingActiveUploads()
                                            print("Downloaded.  Remaining: \(remaining)")
                                            if (remaining == 0) {
                                                self.finishedUploading()
                                            }
                            }
                            else {

                                            let remaining = self.getRemainingActiveUploads()
                                            print("Downloaded.  Remaining: \(remaining)")
                                            if (remaining == 0) {
                                                self.finishedUploading()
                                            }
                            }
                        }
                        else {

                                        let remaining = self.getRemainingActiveUploads()
                                        print("Downloaded.  Remaining: \(remaining)")
                                        if (remaining == 0) {
                                            self.finishedUploading()
                                        }
                        }

                    })
                }
                catch {
                    print("Error: \(error)")
                }
            }

        })
        print("Queuing task \(dltask)")
        dltask.resume()
    }
        session.delegateQueue.suspended = false
    }
    else {
        finishedUploading()
    }
    // resuming queue so all tasks run
}

}

Now this works fine in the following two cases :

  1. Queue is empty -> finishedUploading gets called and uploadAllLinks is run every second to check for items in uploadQueue
  2. Queue has one item -> the one item gets posted, remaining == 0 hence finishedUploading is called

However, whenever the queue has more than one item, the first one gets uploaded, if remaining == 0 fails, and then nothing happens. I don't understand why the for loop is not run for the other items in the queue at this point.

Upvotes: 0

Views: 607

Answers (1)

dgatwood
dgatwood

Reputation: 10417

I suspect that the problem is your 10-second timeout interval. That begins ticking as soon as the data task is created and terminates the task if it remains idle (without receiving new data) for more than ten seconds.

If you have multiple tasks and the OS is only allowed to upload one or two of them at a time, then any task that is queued up waiting to start will never complete. I don't think the documentation mentions that.

In practice, this design makes NSURLSession's queueing less than ideal, and as a result, most folks seem to write their own queues and handle the concurrency limiting on their own, ensuring that each task is created right before it should start running. I would suggest doing something similar:

  • Create a method that starts the next upload in the queue or calls the "everything complete" method if the queue is empty—basically the body of your loop.
  • Instead of the loop itself, call that method to start the first upload.
  • In your completion handler (inside that method), call that method semi-recursively to start the next upload.

Also, 10 seconds is way too short for the timeout interval unless your device is mounted to a wall and is on Wi-Fi with a guaranteed solid signal. Flaky Wi-Fi and weak cellular signals can result in serious latency, so IIRC, the default is 120 seconds, though I've read 60 in various places. Either way, you do not want to use 10 seconds. Such a short timeout would pretty much guarantee that your app will be hopelessly unreliable.

Upvotes: 1

Related Questions