3

I am writing a Swift iOS app (my first, so please bear with me) where I use Swifter HTTP server to process various requests. One such request is an HTTP POST with a JSON array specifying images to download from the web (and do some other stuff, not pertinent to the issue at hand).

I use Alamofire to download the images (this works fine), but I am looking for good (preferably simple) way to wait for all the images to finish downloading before returning a response to the POST request above (since the response has to contain JSON indicating the result, including any failed downloads).

What is a good way to accomplish this (preferably w/o blocking the main thread)?

Here are some snippets to illustrate:

public func webServer(publicDir: String?) -> HttpServer {
  let server = HttpServer()

  server.POST["/images/update"] = { r in
        let images = ...(from JSON array in body)
        let updateResult = ImageUtil.updateImages(images)
        let resultJson: String = Mapper().toJSONString(updateResult, prettyPrint: true)!

        if updateResult.success {
            return .OK(.Text(resultJson))
        }
        return HttpResponse.RAW(500, "Error", nil, { $0.write([UInt8](updateResult.errorMessage.utf8)) })
    }
}

static func updateImages(images: [ImageInfo]) -> UpdateResult {
  let updateResult = UpdateResult()
  for image in images {
    Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
        .validate()
        .response{_, _, _, error in
            if let error = error {
                Log.error?.message("Error downloading file \(image.imageUrl) to \(image.fileName): \(error)")
            } else {
                updateResult.filesDownloaded++
                Log.info?.message("Downloaded file \(image.imageUrl) to \(image.fileName)")
            }}
    }

    return updateResult // It obviously returns before any images finish downloading.  I need to wait until all images have downloaded before I can return an accurate result.
}

Update 1/23/2016, using dispatcher per bbum

This is an attempt to use the dispatcher mechanism, but the call to updateImages still return right away (even when using dispatch_sync).

How can I await the completion of all downloads before returning my HTTP response to the caller?

public func webServer(publicDir: String?) -> HttpServer {
    let server = HttpServer()

    server.POST["/images/update"] = { r in
        let imageDownloader = ImageDownloader()
        imageDownloader.updateimageFiles(adFilesOnServer)
        let resultJson: String = Mapper().toJSONString(imageDownloader.updateResult, prettyPrint: true)!

        if imageDownloader.updateResult.success {
            return .OK(.Text(resultJson))
        }
        return HttpResponse.RAW(500, "Error", nil, { $0.write([UInt8](imageDownloader.updateResult.errorMessage.utf8)) })
    }
}

class ImageDownloader {

    var updateResult = AdUpdateResult()
    private var imageFilesOnServer = [ImageFile]()

    private let fileManager = NSFileManager.defaultManager()
    private let imageDirectoryURL = NSURL(fileURLWithPath: Settings.imageDirectory, isDirectory: true)

    private let semaphore = dispatch_semaphore_create(4)
    private let downloadQueue = dispatch_queue_create("com.acme.downloader", DISPATCH_QUEUE_SERIAL)

    func updateimageFiles(imageFilesOnServer: [ImageFile]) {
        self.imageFilesOnServer = imageFilesOnServer

        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

        for serverFile in imageFilesOnServer {
            downloadImageFileFromServer(serverFile)
        }

        dispatch_sync(downloadQueue) {
            dispatch_sync(dispatch_get_main_queue()) {
                print("done") // It gets here before images have downloaded.
            }
        }
    }

    private func downloadImageFileFromServer(serverFile: ImageFile) {

        let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)

        Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
        .validate()
        .response { _, _, _, error in
            if let error = error {
                Log.error?.message("Error downloading file \(serverFile.imageUrl) to \(serverFile.fileName): \(error)")
            } else {
                self.updateResult.filesDownloaded++
                Log.info?.message("Downloaded file \(serverFile.imageUrl) to \(serverFile.fileName)")
            }
            dispatch_semaphore_signal(self.semaphore)
        }
    }
}
Jim Balo
  • 639
  • 6
  • 22
  • 3
    Don't wait, use a completion handler or a notification. – vadian Jan 23 '16 at 17:30
  • Add each to a queue with a final operation which is dependent and which holds the completion logic – Wain Jan 23 '16 at 17:45
  • @vadian I am processing an HTTP POST request and I need to send a response back that includes the complete result of the downloads. I do not see how to accomplish that without waiting at some point. – Jim Balo Jan 23 '16 at 23:54
  • The `server.POST` method works asynchronously using a completion handler which is executed when the server responds. Send further requests in the completion handler accordingly. – vadian Jan 24 '16 at 07:53
  • @vadian the HTTP server that I am using (Swifter) is not asynchronous, per: https://github.com/glock45/swifter/issues/66. The author recommends using semaphores for this. If you know how to do this asynchronously or how to make it work with semaphores, could you provide some details (see my attempt in the update)? – Jim Balo Jan 24 '16 at 16:23
  • @JimBalo did you find solution ? – Varun Naharia Jun 26 '18 at 12:12

1 Answers1

0

First, you really don't want to be firing off a request-per-image without some kind of a throttle. Semaphores work well for that sort of thing.

Secondly, you need to basically count the number of operations outstanding and then fire a completion handler when they are all done. Or, if new operations can be started at any time, you'll probably want to group operations.

So, pseudo code:

sema = dispatch_semaphore_create(4) // 4 being # of concurrent operations allowed
serialQ = dispatch_queue_create(.., SERIAL)

dispatch_async(serialQ) {
    dispatch_semaphore_wait(sema, FOREVER) // will block if there are 4 in flight already
    for image in images {
        downloader.downloadAsync(image, ...) { // completion
              dispatch_semaphore_signal(sema) // signal that we are done with one
              ... handle downloaded image or error ...
              ... add downloaded images to downloadedImages ...
        }
    }
}

dispatch_async(serialQ) {
     // since serialQ is serial, this will be executed after the downloads are done
    dispatch_async(main_queue()) {
         yo_main_queue_here_be_yer_images(... downloadedImages ...)
    }
}
bbum
  • 162,346
  • 23
  • 271
  • 359
  • Thanks for your response. I tried implementing it, but I must be missing something, because I still cannot get it to wait for all files to download so that I can return a proper HTTP response to the caller. I have updated the question with a simplified version of my code - perhaps you can provide some additional guidance? – Jim Balo Jan 23 '16 at 23:34
  • I just tried replacing the Alamofire download calls with NSThread.sleepForTimeInterval(5) as a test, and it behaves as expected. Perhaps the issue is that Alamofire's download method kicks off on a new thread, making the dispatched download jobs appear to finish immediately? Your advice would be appreciated... – Jim Balo Jan 24 '16 at 18:39
  • @JimBalo unless the download is calling the completion block earlier or repeatedly, that should not be the case. Note that `updateimageFiles()` is going to return immediately as the code is written... and that is exactly what you want it to do. You need to update based on the completed downloads in the block that is dispatched to the main thread. – bbum Jan 24 '16 at 19:52
  • The download is not calling the completion block earlier or repeatedly (I have breakpoints set to confirm this). But it gets to dispatch_sync(dispatch_get_main_queue()) before the download's completion block has been called. I am pulling my hair out on this... – Jim Balo Jan 24 '16 at 23:45
  • I have created a spin-off question here that simply shows the issue of the wait not waiting: http://stackoverflow.com/questions/34983370/swift-dispatch-group-wait-not-waiting If I get a solution to that issue, I think I will be home free on this. – Jim Balo Jan 25 '16 at 00:40