Every 11th execution of NSURLSessionTask takes much longer than others - ios

I'm having a strange behavior in my Swift app, that I currently don't understand.
I have subclassed NSOperation to create different operations that can call Rest-WebServices via NSURLSession / NSURLSessionTask. This works fine in general.
In my case I have to execute many of these operations successively. Let's say I create a "chain" of 30 NSOperations with setting dependencies to execute them one by one.
Now I could reproduce the behavior, that every 11th (?!) execution of such an operation, takes much longer than the others. It seems as if the execution "sleeps" for nearly 10 seconds before it goes on. I can rule out, that the concrete web service call is the issue. Because if I change the order of execution, it is still the 11th operation that "hangs".
Currently I am creating a new instance of NSURLSession (defaultConfiguration) during the execution of every operation. Yesterday I tried to create a static instance of NSURLSession and create the instances of NSURLSessionTask during execution only. And now the "hanger" is gone! Unfortunately I could not do it this way, because the NSURLSessionDelegate has to be different for some operations, but this delegate must be passed during initialization.
Did anyone experience a similar behavior?
First I thought my code is too complex to post. But after Ketans comment, I will give it a try. I have trimmed it down to the most important parts. I hope this helps to show my problem. If you need more detail, please let me know.
class AbstractWebServiceOperation: NSOperation {
// VARIANT 2: Create a static NSURLSession only once --> The "sleep" DOES NOT occur!
static let SESSION = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
init(servicePath:String, httpMethod:String) {
// This is an 'abstract' class, that will be subclassed for concrete webService calls that differ in servicePath for URL, HTTP Method and parameters
}
// Override start() function of NSOperation to do webService call. NSOperations vars (ready, executing, finished) are overridden too, to get NSOperation "waiting" for the webService result. But I don't think it is relevant for the issue. So I did leave it out.
override func start() {
super.start()
// [...]
if let request = createRequest()
{
let task = createTask(request)
task.resume()
}
// [...]
}
// Creates the concrete NSURLRequest by using the service path and HTTP method defined by the concrete subclass.
private func createRequest()-> NSMutableURLRequest? {
// [...]
let webServiceURL = "https://\(self.servicePath)"
let url = NSURL(string: webServiceURL)
let request = NSMutableURLRequest(URL: url!)
request.timeoutInterval = 60
request.HTTPMethod = self.httpMethod
request.addValue("application/json;charset=UTF-8", forHTTPHeaderField: "Content-Type")
request.addValue("application/json;charset=UTF-8", forHTTPHeaderField: "Accept")
return request;
}
// Creates the concrete NSURLSessionTask for the given NSURLRequest (using a completionHandler defined by getCompletionHandler())
func createTask(request:NSURLRequest) -> NSURLSessionTask
{
// VARIANT 1: Create a new NSURLSession every time a AbstractWebServiceOperation is executed --> The "sleep" occurs!
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: nil)
return session.dataTaskWithRequest(request, completionHandler:getCompletionHandler())
// VARIANT 2: Create a static NSURLSession only once --> The "sleep" DOES NOT occur!
return AbstractWebServiceOperation.SESSION.dataTaskWithRequest(request, completionHandler:getCompletionHandler())
}
// Returns the completion handler for the NSURLSessionTask (may be overriden in subclass)
func getCompletionHandler() -> (NSData?, NSURLResponse?, NSError?) -> Void
{
return completionHandler
}
// Default completion handler
lazy var completionHandler:(NSData?, NSURLResponse?, NSError?) -> Void = {(data : NSData?, response : NSURLResponse?, error : NSError?) in
// default completion handling
}
}

Awww... I simply forgot to call session.finishTasksAndInvalidate() to invalidate the session after my webService call is done.
That solves my problem!

Related

taskWillPerformHTTPRedirection never called in Alamofire 5

Updating Alamofire to 5.0.4. As the title says taskWillPerformHTTPRedirection is never called.
In Alamofire 4.x we could do something like:
let sessionDelegate = request.session.delegate as! Alamofire.SessionDelegate
sessionDelegate.taskWillPerformHTTPRedirection = { session, task, response, request in
if let url = task.currentRequest?.url {
// look at redirected url & act accordingly
}
}
}
A request's session/delegate has been overhauled in Alamofire 5 and is no longer directly accessible from the request. More specifically, taskWillPerformHTTPRedirection is a closure callback on ClosureEventMonitor. As a sanity check, I tested using some of the other closure callbacks.. and they worked.
// ClosureEventMonitor
let monitor = ClosureEventMonitor()
monitor.requestDidCreateTask = { request, task in
// Event fires
}
let monitor2 = ClosureEventMonitor()
monitor2.taskWillPerformHTTPRedirection = { sess, task, resp, req in
// Event Never fires
}
monitor2.requestDidFinish = { request in
// Event Fires
}
// Set up Session
var session: Session? = Session(startRequestsImmediately: false, eventMonitors: [monitor, monitor2])
let url = URL(string: "https://google.com")!
let urlRequest = URLRequest(url: url)
let trequest = session?.request(urlRequest)
For reference this code is being fired from my AppDelegate func application(_ application: UIApplication, continue userActivity: NSUserActivity for handling deep/universal links.
I'm not exactly sure what I'm missing here. Any help is greatly appreciated. Thank you for your time.
There are three things here:
First, session?.request(urlRequest) will never actually make a request, since you never call resume() (or attach a response handler).
Second, using a one off Session like that is not recommended. As soon as the Session goes out of scope all requests will be cancelled.
Third, EventMonitors cannot interact with the request pipeline, they're only observational. Instead, use Alamofire 5's new RedirectHandler protocol or Redirector type to handle redirects. There is more in our documentation. A simple implementation that customizes the action performed would be:
let redirector = Redirector(behavior: .modify { task, request, response in
// Customize behavior.
})
session?.request(urlRequest).redirect(using: redirector)

Dispatch Group not allowing Alamofire request to execute

I'm using DispatchGroup to wait until a callback for one of my functions executes before continuing. Within that function, I'm calling Alamo fire get request. My issue occurs when I introduce the DispatchGroup, the AlamoFire closure never gets executed.
Sample
let group = DispatchGroup()
group.enter()
Networking.getInfo(userID: userID) { info in
group.leave()
}
group.wait()
Networking class:
static func getInfo(userID: Int, completion: #escaping(_ info: String) -> Void) {
// Program reaches here
Alamofire.request("https://someurl.com").responseJSON { response in
// Program does NOT get here
if let json = response.result.value {
completion("Successful request")
} else {
completion("Some Error")
}
}
}
When I don't use the DispatchGroup, it works fine. When I DO use the DispatchGroup, The getInfo function starts, but the closure of the Alamofire request never gets executed.
Not sure I'm right, but I suspect that the Alamofire response is being queued on the same queue (main) that the group has suspended (wait()). Because the queue is suspended, the completion closure never executes.
Manually writing asynchronous code like this can be quite tricky. My suggestion would be to use any one of the asynchronous libraries out there which can help with this. My personal favourite being PromiseKit which also has specific extensions to support Alamofire. Projects like this can take a lot of the headache out of asynchronous code. They may take some time to get your head around their paradigms, but it's worth doing.
I faced the same problem. In that case I used URLSession request to get ride of it. This API enables your app to perform background downloads when your app isn’t running or, in iOS, while your app is suspended.
https://developer.apple.com/documentation/foundation/urlsession
let request = try URLRequest(url: url, method: .get, headers: headers)
Alamofire.request(request) { response in
...
...
}
I changed it like this :-
let request = try URLRequest(url: url, method: .get, headers: headers)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
...
...
}
task.resume()
And then it worked fine.

Cancel Alamofire file download if this file is already exists

How to cancel Alamofire request if the downloaded file is already exists in documents folder?
Here is the code for request:
Alamofire.download(.GET, fileUrls[button.tag], destination: { (temporaryURL, response) in
if let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0] as? NSURL {
let fileURL = directoryURL.URLByAppendingPathComponent(response.suggestedFilename!)
self.localFilePaths[button.tag] = fileURL
if NSFileManager.defaultManager().fileExistsAtPath(fileURL.path!) {
NSFileManager.defaultManager().removeItemAtPath(fileURL.path!, error: nil)
}
return fileURL
}
println("temporaryURL - \(temporaryURL)")
self.localFilePaths[button.tag] = temporaryURL
return temporaryURL
}).progress { _, totalBytesRead, totalBytesExpectedToRead in
println("\(totalBytesRead) - \(totalBytesExpectedToRead)")
dispatch_async(dispatch_get_main_queue()) {
self.progressBar.setProgress(Float(totalBytesRead) / Float(totalBytesExpectedToRead), animated: true)
if totalBytesRead == totalBytesExpectedToRead {
self.progressBar.hidden = true
self.progressBar.setProgress(0, animated: false)
}
}
}.response { (_, _, data, error) in
let previewQL = QLReaderViewController()
previewQL.dataSource = self
previewQL.currentPreviewItemIndex = button.tag
self.navigationController?.pushViewController(previewQL, animated: true)
}
I've also tried to create a request variable var request: Alamofire.Request? and then cancel request?.cancel() it if that file exists but it doesn't work.
Can someone help me to solve this issue?
Rather than cancelling the request, IMO you shouldn't make it in the first place. You should do the file check BEFORE you start the Alamofire request.
If you absolutely feel you need to start the request, you can always cancel immediately after starting the request.
var shouldCancel = false
let request = Alamofire.request(.GET, "some_url") { _, _ in
shouldCancel = true
}
.progress { _, _, _ in
// todo...
}
.response { _, _, _ in
// todo...
}
if shouldCancel {
request.cancel()
}
TL; DR: Canceling a request is a bit cumbersome in many cases. Even Alamofire, as far as I know, does not guarentee that request will be cancelled upon your request, immediately. However, you may use dispatch_suspend or NSOperation in order to overcome this.
Grand Central Dispatch (GCD)
This way utilizes functional programming.
Here we enlight our way with low-level programming. Apple introduced a good library, aka GCD, to do some thread-level programming.
You cannot cancel a block, unless... you suspend a queue (if it is not main or global queue).
There is a C function called dispatch_suspend, (from Apple's GCD Reference)
void dispatch_suspend(dispatch_object_t object);
Suspends the invocation of block objects on a dispatch object.
And you can also create queues (who are dispatch_object_ts) with dispatch_queue_create.
So you can do your task in user created queue, and you may suspend this queue in order to prevent CPU from doing something unnecessary.
NSOperation (also NSThread)
This way utilizes functional programming over object-oriented interface.
Apple also introduced NSOperation, where object-oriented programming may be object, whereas it is easier to cope with.
NSOperation is an abstract class, which associates code and data, according to the Apple's documentation.
In order to use this class, you should either use one of its defined subclasses, or create your own subclass: In your case particularly, I suppose NSBlockOperation is the one.
You may refer to this code block:
let block = NSBlockOperation { () -> Void in
// do something here...
}
// Cancel operation
block.cancel()
Nevertheless, it also does not guarantee stopping from whatever it is doing. Apple also states that:
This method does not force your operation code to stop. Instead, it updates the object’s internal flags to reflect the change in state. If the operation has already finished executing, this method has no effect. Canceling an operation that is currently in an operation queue, but not yet executing, makes it possible to remove the operation from the queue sooner than usual.
If you want to take advantage of flags, you should read more: Responding to the Cancel Command

NSUrlConnection synchronous request without following redirections

Problem
I need to execute a synchronous HTTP request, without following redirects, preferably without using instance variables, since this is to be incorporated into the j2objc project.
What have I tried
I have tried using NSURLConnection sendSynchronousRequest, which unfortunately cannot easily be told not to follow redirects.
Background
Before telling me that I should not use synchronous requests, please bear in mind that this code is for emulating Java's HttpUrlConnection, which is inherently synchronous in behavior, for the j2objc project. The implementation of IosHttpUrlConnections' native makeSynchronousRequest currently always follows redirects. It should respect the HttpUrlConnection.instanceFollowRedirects field.
Further research conducted
When using NSUrlConnection in asynchronous mode, a delegate method is called, which allows for enabling/disabling redirects. However, I need synchronous operation.
This answer on NSUrlconnection: How to wait for completion shows how to implement sendSynchronousRequest using an async request. However, I haven't been able to modify it to use a delegate, and thus haven't been able to not follow redirects.
I hope you can help me
You can use a NSURLSession with a semaphore, create like this:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
NSURLSessionTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (data)
{
// do whatever you want with the data here
}
else
{
NSLog(#"error = %#", error);
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// but have the thread wait until the task is done
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
And you have to implement the following method of NSURLSessionTaskDelegate, and call the completionHandler block passing null to stop the redirect.
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler
I guess I'll pick up where they left off, but in Swift since it's so many years later.
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let semaphore = DispatchSemaphore(value: 0)
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 10
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
// Redirects to google.com
guard let url = URL(string: "https://bit(dot)ly/19BiSHW") else {
return
}
var data: Data?
var response: URLResponse?
var error: Error?
let task = session.dataTask(with: url) { (innerData, innerResponse, innerError) in
// For clarity, we'll call this the data task's completion closure
// Pass the data back to the calling scope
data = innerData
response = innerResponse
error = innerError
semaphore.signal()
}
task.resume()
if semaphore.wait(timeout: .now() + .seconds(15)) == .timedOut {
// The task's completion closure wasn't called within the time out, handle appropriately
} else {
if let e = error as NSError? {
if e.domain == NSURLErrorDomain && e.code == NSURLErrorTimedOut {
print("The request timed out")
}
return
}
if let d = data {
// do whatever you want with the data here, such as print it
// (the data is the HTTP response body)
print(String.init(data: d, encoding: .utf8) ?? "Response data could not be turned into a string")
return
}
if let r = response {
print("No data and no error, but we received a response, we'll inspect the headers")
if let httpResponse = r as? HTTPURLResponse {
print(httpResponse.allHeaderFields)
}
}
}
}
}
extension ViewController: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Swift.Void) {
// Inside the delegate method, we will call the delegate's completion handler
// completionHandler: A block that your handler should call with
// either the value of the request parameter, a modified URL
// request object, or NULL to refuse the redirect and return
// the body of the redirect response.
// I found that calling the block with nil only triggers the
// return of the body of the redirect response if the session is ephemeral
// Calling this will trigger the data task's completion closure
// which signals the semaphore and allows execution to continue
completionHandler(nil)
}
}
What the code is doing:
It is creating an inherently asynchronous task (URLSessionTask), telling it to being execution by calling resume(), then halting the current execution context by waiting on a DispatchSemaphore. This is trick I've seen used, and personally used on many occasions to make something asynchronous behave in a synchronous fashion.
The key point to make is that the code stops execution in the current context. In this example, that context is the main thread (since it is in a UIViewController method), which is generally bad practice. So, if your synchronous code never continues executing (because the semaphore is never signaled) then you UI thread will be stopped forever causing the UI to be frozen.
The final piece is the implementation of the delegate method. The comments suggest that calling completionHandler(nil) should suffice and the documentation supports that. I found that this is only sufficient if you have an ephemeral URLSessionConfiguration. If you have the default configuration, the data task's completion closure doesn't get invoked, so the semaphore never gets signaled, therefore the code to never moves forward. This is what was causing the commenter's/asker's problems of a frozen UI.

How to find and cancel a task in NSURLSession?

I'm using an NSURLSession object to load images in my application. That could be loading several images simultaneously.
In some moments I need to cancel the loading of one specific image and continue loading others.
Could you suggest the correct way to do that?
To get tasks list you can use NSURLSession's method
- (void)getTasksWithCompletionHandler:(void (^)(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks))completionHandler;
Asynchronously calls a completion callback with all outstanding data,
upload, and download tasks in a session.
Then check task.originalRequest.URL for returned tasks to find the one you want to cancel.
Based on all the answers below, I'd go for something like this:
Swift 5
func cancelTaskWithUrl(_ url: URL) {
URLSession.shared.getAllTasks { tasks in
tasks
.filter { $0.state == .running }
.filter { $0.originalRequest?.url == url }.first?
.cancel()
}
}
You also probably want to account for your task completion handler, since canceling the task will result Error in that completion handler.
Hope below code help.
-(IBAction)cancelUpload:(id)sender {
if (_uploadTask.state == NSURLSessionTaskStateRunning) {
[_uploadTask cancel];
}
}
Swift 3.0 version of #Avt's answer to get the task list. Use getTasksWithCompletionHandler.
func getTasksWithCompletionHandler(_ completionHandler: #escaping ([URLSessionDataTask],
[URLSessionUploadTask],
[URLSessionDownloadTask]) -> Void) {
}
The returned arrays contain any tasks that you have created within the
session, not including any tasks that have been invalidated after
completing, failing, or being cancelled.
I suggest two methods:
Put the list of NSURLSessionTask in an array. In case you don't know exactly how many images you would get. Though you have to know the index of session in order to cancel it.
If you get a limited number of images. Just use a set of NSURLSessionTask as global variables so you can access to cancel it anywhere in your class.
I think you should do this...
First, keep track of your requests per xib
var download_requests = [NSURLSession]()
Then, whenever you make a request, append your request to your array like so,
let s = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
if let url = NSURL(string: "http://my.url.request.com")
{
download_requests.append(s)
s.dataTaskWithURL(url)
{ (data, resp, error) -> Void in
// ....
}
}
Then whenever you want to cancel any outstanding requests, (let's say on viewDidDisappear), do
override func viewDidDisappear(animated: Bool)
{
super.viewDidDisappear(animated)
//stop all download requests
for request in download_requests
{
request.invalidateAndCancel()
}
}

Resources