Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSOperationQueue and background support

I have some problems on elaborating a useful strategy to support background for NSOperationQueue class. In particular, I have a bunch of NSOperations that perform the following actions:

  • Download a file from the web
  • Parse the file
  • Import data file in Core Data

The operations are inserted into a serial queue. Once an operation completes, the next can start.

I need to stop (or continue) the operations when the app enters the background. From these discussions ( Does AFNetworking have backgrounding support? and Queue of NSOperations and handling application exit ) I see the best way is to cancel the operations and the use the isCancelled property within each operation. Then, checking the key point of an operation against that property, it allows to roll back the state of the execution (of the running operation) when the app enters background.

Based on Apple template that highlights background support, how can I manage a similar situation? Can I simply cancel the operations or wait the current operation is completed? See comments for details.

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    bgTask = [application beginBackgroundTaskWithExpirationHandler:^{

        // Do I have to call -cancelAllOperations or
        // -waitUntilAllOperationsAreFinished or both?

        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // Start the long-running task and return immediately.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // What about here?

        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}

Thank you in advance.

Edit

If NSOperation main method perform the code below, how is it possible to follow the Responding to Cancellation Events pattern?

- (void)main
{
    // 1- download

    // 2- parse
    // 2.1 read file location
    // 2.2 load into memory

    // 3- import
    // 3.1 fetch core data request
    // 3.2 if data is not present, insert it (or update)
    // 3.3 save data into persistent store coordinator
}

Each method I described contains various steps (non atomic operations, except the download one). So, a cancellation could happen within each of these step (in a not predefined manner). Could I check the isCancelled property before each step? Does this work?

Edit 2 based on Tammo Freese' edit

I understand what do you mean with your edit code. But the thing I'm worried is the following. A cancel request (the user can press the home button) can happen at any point within the main execution, so, if I simply return, the state of the operation would be corrupted. Do I need to clean its state before returning? What do you think?

The problem I described could happen when I use sync operations (operations that are performed in a sync fashion within the same thread they run). For example, if the main is downloading a file (the download is performed through +sendSynchronousRequest:returningResponse:error) and the app is put in background, what could it happen? How to manage such a situation?

// download
if ([self isCancelled])
    return;

// downloading here <-- here the app is put in background

Obviously, I think that when the app is then put in foreground, the operation is run again since it has been cancelled. In other words, it is forced to not maintain its state. Am I wrong?

like image 796
Lorenzo B Avatar asked Dec 06 '22 11:12

Lorenzo B


1 Answers

If I understand you correctly, you have a NSOperationQueue and if your application enters the background, you would like to

  1. cancel all operations and
  2. wait until the cancellations are processed.

Normally this should not take too much time, so it should be sufficient to do this:

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    [_queue cancelAllOperations];
    [_queue waitUntilAllOperationsAreFinished];
}

The definition of "too much time" here is approximately five seconds: If you block -applicationDidEnterBackground: longer than that, your app will be terminated and purged from memory.

Let's say that finishing the cancelled operations takes longer than 5 seconds. Then you have to do the waiting in the background (see the comments for explanations):

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // If this block is called, our background time of normally 10 minutes
        // is almost exceeded. That would mean one of the cancelled operations
        // is not finished even 10 minutes after cancellation (!).
        // This should not happen.
        // What we do anyway is tell iOS that our background task has ended,
        // as otherwise our app will be killed instead of suspended.
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // Normally this one is fast, so we do it outside the asynchronous block.
    [_queue cancelAllOperations];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // Wait until all the cancelled operations are finished.
        [_queue waitUntilAllOperationsAreFinished];

        // Dispatch to the main queue if bgTask is not atomic
        dispatch_async(dispatch_get_main_queue(), ^{
            [application endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
        });
    });
}

So basically we tell iOS that we need some time to perform a task, and when the task is finished of the time runs out, we tell iOS that our task has ended.

Edit

To answer the question in your edit: To respond to cancellation, just check for cancellation whenever you can, and return from the -main method. A cancelled operation is not immediately finished, but it is finished when -main returns.

- (void)main
{
    // 1- download
    if ([self isCancelled]) return;

    // 2- parse
    // 2.1 read file location

    // 2.2 load into memory
    while (![self isCancelled] && [self hasNextLineToParse]) {
        // ...
    }

    // 3- import

    // 3.1 fetch core data request
    if ([self isCancelled]) return;


    // 3.2 if data is not present, insert it (or update)
    // 3.3 save data into persistent store coordinator
}

If you do not check for the cancelled flag at all in -main, the operation will not react to cancellation, but run until it is finished.

Edit 2

If an operation gets cancelled, nothing happens to it except that the isCancelled flag is set to true. The code above in my original answer waits in the background until the operation has finished (either reacted to the cancellation or simply finished, assuming that it does not take 10 minutes to cancel it).

Of course, when reacting to isCancelled in our operation you have to make sure that you leave the operation in a non-corrupted state, for example, directly after downloading (just ignoring the data), or after writing all data.

You are right, if an operation is cancelled but still running when you switch back to the foreground, that operation will finish the download, and then (if you programmed it like that) react to cancel and basically throw away the downloaded data.

What you could do instead is to not cancel the operations, but wait for them to finish (assuming they take less than 10 minutes). To do that, just delete the line [_queue cancelAllOperations];.

like image 77
Tammo Freese Avatar answered Jan 04 '23 21:01

Tammo Freese