Cancelling an Alamofire Request Wrapped in Nsoperation Causes Multiple Kvo

Cancelling an Alamofire Request Wrapped In NSOperation Causes Multiple KVO?

I don't find the multiple KVN behavior you describe as that surprising. There's nothing in the documentation that says that when it cancels all operations, that a single KVN on operations will result. In fact, one might safely infer that the behavior you describe should be expected (because it doesn't preemptively kill all of those worker threads, but rather sends a cancel message to each, and each operation is responsible for responding to that in its own time; and I wouldn't expect operations to be updated until the operation finally actually finishes).

Personally, I would advise retiring this observer pattern entirely. Your code should not be contingent upon whether NSOperationQueue removes all of operations at once or not. I would instead suggest that you instead rely upon your existing downloadImageCompletionHandler closure, calling it whether the request completed or not. Just have the closure look at the error object to figure out whether it was canceled or whether it failed for some other reason.


If your intent is to know when all of these operations are done, I wouldn't rely on operations KVN. Instead, I might create a completion operation, dependent upon all of those other requests:

let completionOperation = NSBlockOperation() {                    // create completion operation
// do whatever you want here
}

for imgLink in imgLinks {
let operation = DownloadImageOperation(URLString: imgLink) { responseObject, error in
if error != nil {
if error!.code == NSURLErrorCancelled && error!.domain == NSURLErrorDomain {
println("everything OK, just canceled")
} else {
println("error=\(error)")
}
}
if responseObject != nil {
println("\(responseObject?.absoluteString) downloaded.")
}
}

completionOperation.addDependency(operation) // add dependency

testAlamofireObserver!.queue.addOperation(operation)
}

NSOperationQueue.mainQueue().addOperation(completionOperation) // schedule completion operation on some other queue (so that when I cancel everything on that other queue, I don't cancel this, too)

Alamofire cancel a request out of multiple requests

According to the documentation cancel would:

Cancels the underlying task, producing an error that is passed to any registered response handlers.

Therefore, add a response handler to your request and see whether error messages are passed to them. If so, it means that the cancel operates correctly.

Alamofire - request.cancel() is also cancelling every other request from that server?

Never mind, it was a server issue. Reconfigured it and everything is working.

Abort an Alamofire download request before it completes

Error -999 is the cancellation error returned by the system. It just means the ongoing URLSessionTask was cancelled before completing. We've made it an explicit error in Alamofire 5, but it's still considered an error state, so if you need separate handling, you can do so in your response closure.

suspend and resume alamofire download request - large file

As explained on GitHub, resumeData only includes the data necessary to resume a download, not the actual download data itself, so it's perfectly safe to keep it around in memory. You can parse the value to get the URL of the partially downloaded data, but it's not a formatted encoding, so it's not really appropriate for Alamofire to parse it directly.

NSURLSession concurrent requests with Alamofire

Yes, this is expected behavior. One solution is to wrap your requests in custom, asynchronous NSOperation subclass, and then use the maxConcurrentOperationCount of the operation queue to control the number of concurrent requests rather than the HTTPMaximumConnectionsPerHost parameter.

The original AFNetworking did a wonderful job wrapping the requests in operations, which made this trivial. But AFNetworking's NSURLSession implementation never did this, nor does Alamofire.


You can easily wrap the Request in an NSOperation subclass. For example:

class NetworkOperation: AsynchronousOperation {

// define properties to hold everything that you'll supply when you instantiate
// this object and will be used when the request finally starts
//
// in this example, I'll keep track of (a) URL; and (b) closure to call when request is done

private let urlString: String
private var networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)?

// we'll also keep track of the resulting request operation in case we need to cancel it later

weak var request: Alamofire.Request?

// define init method that captures all of the properties to be used when issuing the request

init(urlString: String, networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)? = nil) {
self.urlString = urlString
self.networkOperationCompletionHandler = networkOperationCompletionHandler
super.init()
}

// when the operation actually starts, this is the method that will be called

override func main() {
request = Alamofire.request(urlString, method: .get, parameters: ["foo" : "bar"])
.responseJSON { response in
// do whatever you want here; personally, I'll just all the completion handler that was passed to me in `init`

self.networkOperationCompletionHandler?(response.result.value, response.result.error)
self.networkOperationCompletionHandler = nil

// now that I'm done, complete this operation

self.completeOperation()
}
}

// we'll also support canceling the request, in case we need it

override func cancel() {
request?.cancel()
super.cancel()
}
}

Then, when I want to initiate my 50 requests, I'd do something like this:

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

for i in 0 ..< 50 {
let operation = NetworkOperation(urlString: "http://example.com/request.php?value=\(i)") { responseObject, error in
guard let responseObject = responseObject else {
// handle error here

print("failed: \(error?.localizedDescription ?? "Unknown error")")
return
}

// update UI to reflect the `responseObject` finished successfully

print("responseObject=\(responseObject)")
}
queue.addOperation(operation)
}

That way, those requests will be constrained by the maxConcurrentOperationCount, and we don't have to worry about any of the requests timing out..

This is an example AsynchronousOperation base class, which takes care of the KVN associated with asynchronous/concurrent NSOperation subclass:

//
// AsynchronousOperation.swift
//
// Created by Robert Ryan on 9/20/14.
// Copyright (c) 2014 Robert Ryan. All rights reserved.
//

import Foundation

/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `completeOperation()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `completeOperation()` is called.

public class AsynchronousOperation : Operation {

private let stateLock = NSLock()

private var _executing: Bool = false
override private(set) public var isExecuting: Bool {
get {
return stateLock.withCriticalScope { _executing }
}
set {
willChangeValue(forKey: "isExecuting")
stateLock.withCriticalScope { _executing = newValue }
didChangeValue(forKey: "isExecuting")
}
}

private var _finished: Bool = false
override private(set) public var isFinished: Bool {
get {
return stateLock.withCriticalScope { _finished }
}
set {
willChangeValue(forKey: "isFinished")
stateLock.withCriticalScope { _finished = newValue }
didChangeValue(forKey: "isFinished")
}
}

/// Complete the operation
///
/// This will result in the appropriate KVN of isFinished and isExecuting

public func completeOperation() {
if isExecuting {
isExecuting = false
}

if !isFinished {
isFinished = true
}
}

override public func start() {
if isCancelled {
isFinished = true
return
}

isExecuting = true

main()
}

override public func main() {
fatalError("subclasses must override `main`")
}
}

/*
Abstract:
An extension to `NSLocking` to simplify executing critical code.

Adapted from Advanced NSOperations sample code in WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/
Adapted from https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip
*/

import Foundation

extension NSLocking {

/// Perform closure within lock.
///
/// An extension to `NSLocking` to simplify executing critical code.
///
/// - parameter block: The closure to be performed.

func withCriticalScope<T>(block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}

There are other possible variations of this pattern, but just ensure that you (a) return true for asynchronous; and (b) you post the necessary isFinished and isExecuting KVN as outlined the Configuring Operations for Concurrent Execution section of the Concurrency Programming Guide: Operation Queues.



Related Topics



Leave a reply



Submit