Sunday, January 1, 2012

Automating Recovery after Failure

A Simple Download Manager
Introduction
This report is aimed at discussing a design for an iOS application, which downloads files from the internet. Based on this design, the application is able to recover itself from failures, which disrupt the download process and make it stop. The discussion in this report covers the areas related to how the download states are saved in case of a failure, and how the application uses the saved information to resume previously in progress downloads. The example application uses ASIHTTPRequest library to perform network activities (in this case, simply downloading files), and uses ASINetworkQueue to manage multiple downloads. (A link to the source of the project discussed in this report is provided at the end of the report)

Application general description
The example download manager application uses two buttons to initiate downloads for a 5MB and a 10MB files. To enable an ASIHTTPRequest object to be able to resume not finished downloads, temporary download path can be set for partial downloads. Figure 1, shows the application in 3 different states.

Figure 1: The download manager application in 3 states: No downloads in queue, 1 download in progress and 2 download in progress

Note that in figure 1, when downloads are in progress, network activity indicator is animating on the status bar. Figure 2 shows the partially downloaded files on the hard drive. Note that the .download extension indicates that downloads are not completed.

Figure 2: partially downloaded files (download is still in progress)

To provide the ability to recover in the download manager application, information of download requests is kept in an array of dictionaries, called dlTracker (NSMutableArray* dlTracker). Every request has a unique identifier so that it would be recognizable in the entire application. For this purpose, the tag property of ASIHTTPRequest objects is used. Download buttons code snippets are provided below. (A link to the source of the project is provided at the end of the report)

- (IBAction)startDownloadFiveMBBtnPressed:(id)sender
{
    if (file5MBDownloadInProgress)
        return;
    NSURL *url = [NSURL URLWithString:@"http://download.thinkbroadband.com/5MB.zip"];
                 
    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];

    NSString *downloadPath = [path stringByAppendingString:@"/5MB.zip"];   
    [request setDownloadDestinationPath:downloadPath];
   
    [request setTemporaryFileDownloadPath:[path stringByAppendingString:@"/5MB.zip.download"]];
    [request setAllowResumeForFileDownloads:YES];
   
    [request setDelegate:self];
    [request setDownloadProgressDelegate:progressViewFive];
    [request setShowAccurateProgress:YES];
   
    request.tag = 5;
    //[request startAsynchronous];
   
    [self takeRequestInfo:request];
    [queue addOperation:request];
    [queue go];
}

- (IBAction)startDownloadTenMBBtnPressed:(id)sender
{
    if (file10MBDownloadInProgress)
        return;
    NSURL *url = [NSURL URLWithString:@"http://download.thinkbroadband.com/10MB.zip"];   
   
    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
   
    NSString *downloadPath = [path stringByAppendingString:@"/10MB.zip"];   
    [request setDownloadDestinationPath:downloadPath];
   
    [request setTemporaryFileDownloadPath:[path stringByAppendingString:@"/10MB.zip.download"]];
    [request setAllowResumeForFileDownloads:YES];
   
    [request setDelegate:self];
    [request setDownloadProgressDelegate:progressViewTen];
    [request setShowAccurateProgress:YES];
   
    request.tag = 10;
    //[request startAsynchronous];
   
    [self takeRequestInfo:request];
    [queue addOperation:request];
    [queue go];
}
Code snippet 1: IBAction methods for initiating downloads
The application going to the background, network getting disconnected, and force stop by user; will be discussed as failure scenarios in the following. It should be mentioned that ASIHTTPRequest does not stop its file transferring process in the background. However, in this design, downloads are forced to stop in background, to better show the functionality of the application. A better design would be downloads remain in progress in background and be saved and stopped when the application is about to terminate.


First failure scenario: Application going to the background
When the application is about to enter background, downloads are stopped and the data being kept in dlTracker array is written to a file called “dlTracker.if”. Information kept for each download request includes, URL, download path, download temporary path and request unique identifier. Figures 3, shows that the dlTracker.if file comes into existence when the application enters background, and figure 4, shows the dlTracker file format.

Figure 3: dlTracker.if file is created as the application enters background

Figure 4: dlTracker file format

It should be mentioned that dlTracker.if file is only created when there are download requests in download queue. Therefore, when re-entering foreground, the application uses this condition, which is the existence of dlTracker.if file, to determine if there were any download requests in progress when the application entered background. So, on changing state from background to foreground, if dlTracker.if file exists, user is asked for confirmation on resuming downloads. If user chooses to continue downloads, the requests are recreated based on their information in dlTracker.if file, and are resumed from the point indicated by partially downloaded files. As downloads begin to continue, dlTracker.if file is also removed.

Figure 5: dlTracker.if file is erased if user decides to continue.

If any of downloads finishes, its data will be removed from dlTracker array. Therefore, in case of any interruption, the finished process information will not be written to file. This is how not attempting to resume completed processes by the application is handled.

Figure 6: one of the download requests has been completed

Figure 7: dlTracker.if file does not contain any information about the finished process

Code snippets for parts discussed in this section are provided below. (A link to the source of the project is provided at the end of the report)

- (void)takeRequestInfo:(ASIHTTPRequest*)request
{
    NSString* requestTag = [[NSString alloc] initWithFormat:@"%d",request.tag];
    NSString* url = [[NSString alloc] initWithString:[[request url] description]] ;
   
    NSArray* objects = [[NSArray alloc] initWithObjects:
                        requestTag,url,
                        [request downloadDestinationPath],
                        [request temporaryFileDownloadPath],nil];

    NSArray* keys = [[NSArray alloc] initWithObjects:
                        @"tag",@"url",
                        @"downloadPath",
                        @"tmpDownloadPath",nil];
   
    [requestTag release];                       
    [url release];
                       
    NSDictionary* requestInfo = [[NSDictionary alloc] initWithObjects:objects forKeys:keys];
   
    [objects release];
    [keys release];
   
    [dlTracker addObject:requestInfo];
    [requestInfo release];
}
Code snippet 2: Tracking requests in an NSMutableArray.

- (void)willResignActive:(NSNotification*)notification
{
    [self performSelector:@selector(saveRequestInfo)];
}

- (void)saveRequestInfo
{
    if ([dlTracker count]>0)
    {
        if (queue != nil)
            [queue cancelAllOperations];
        [dlTracker writeToFile:[path stringByAppendingString:@"/dlTracker.if"] atomically:YES];
        [dlTracker removeAllObjects];   
    }
}

- (void)willEnterForeground:(NSNotification*)notification
{
    [self performSelector:@selector(initializeNetworkOperation)];
}
Code snippet 3: Application changing state between background and foreground.

- (void)initializeNetworkOperation
{
    if (reachability == nil)
        reachability = [[Reachability reachabilityForInternetConnection] retain];
    [reachability stopNotifier];
    [reachability startNotifier];

    if ([reachability currentReachabilityStatus] == NotReachable)
    {
        [self performSelector:@selector(updateNetworkStatus:)
                              withObject:@"No Internet availability"];
        return;
    }
    [self performSelector:@selector(updateNetworkStatus:)
                              withObject:@"Internet available"];
   
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [[NSNotificationCenter defaultCenter] addObserver:self       
                             selector:@selector(reachabilityChanged:)
                             name:kReachabilityChangedNotification object:nil];   
   
    if (queue == nil)
    {
        queue = [[ASINetworkQueue alloc] init];
        [queue setDelegate:self];
        [queue setDownloadProgressDelegate:progressViewQ];
        [queue setShowAccurateProgress:YES];
        [queue setQueueDidFinishSelector:@selector(queueComplete:)];   
        [queue setRequestDidStartSelector:@selector(queueInProgress:)];
    }
   
    [[NSNotificationCenter defaultCenter] addObserver:self
                    selector:@selector(willEnterForeground:)                                                 
                    name:@"UIApplicationWillEnterForegroundNotification" 
                    object:nil];   
    [[NSNotificationCenter defaultCenter] addObserver:self
                    selector:@selector(willResignActive:)
                    name:@"UIApplicationWillResignActiveNotification"
                    object:nil];
   
    if (dlTracker == nil)
        dlTracker = [[NSMutableArray alloc] init];
   
    if ([self performSelector:@selector(checkForIncompleteRequests)])
    {
        UIAlertView* alertView = [[UIAlertView alloc]
                                   initWithTitle:@"Incomplete Downloads"
                                   message:@"Continue incomplete downloads?"
                                   delegate:self
                                   cancelButtonTitle:@"No"
                                   otherButtonTitles:@"Yes",nil];
        [alertView show];
        [alertView release];
    }   
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 1)
        [self performSelector:@selector(recoverRequests)];
}

- (void)recoverRequests
{
    NSMutableArray* mArray = [[NSMutableArray alloc] initWithContentsOfFile:[path stringByAppendingString:@"/dlTracker.if"]];
   
    NSFileManager* fileManager = [NSFileManager defaultManager];
    [fileManager removeItemAtPath:[path stringByAppendingString:@"/dlTracker.if"] error:nil];
   
    for (NSDictionary* requestInfo in mArray)
    {
        NSURL *url = [[NSURL alloc] initWithString:[requestInfo objectForKey:@"url"]];
        ASIHTTPRequest *request = [[ASIHTTPRequest alloc] initWithURL:url];
        [url release];
       
        [request setDownloadDestinationPath:[requestInfo objectForKey:@"downloadPath"]];
        [request setTemporaryFileDownloadPath:[requestInfo objectForKey:@"tmpDownloadPath"]];
       
        NSNumber* requestTag = [requestInfo objectForKey:@"tag"];
        request.tag = [requestTag intValue];
       
        [request setDelegate:self];
        [request setAllowResumeForFileDownloads:YES];
       
        if (request.tag==5)
            [request setDownloadProgressDelegate:progressViewFive];
        else if (request.tag == 10)
            [request setDownloadProgressDelegate:progressViewTen];
        
        [request setShowAccurateProgress:YES];
       
        [self takeRequestInfo:request];
        [queue addOperation:request];
        [request release];
    }
   
    [mArray release];
   
    if ( [queue operationCount]>0 )
        [queue go];
}
Code snippet 4: Network operation initialization including checking for incomplete downloads and initiating downloads recovery.

Second failure scenario: Internet is not available
When the application is initialized, an object of type Reachability is created, which is constantly observing Internet connection. By every change in network status, Reachability object notifies the application of the change. This is where network failures are actually handled. As soon as the application is notified by a change in network status, and if it determines that the internet is not available anymore, dlTracker.if file is generated; containing the data related to active requests. When internet connection is back, saved requests are recovered through the same process as discussed before. Figure 8, illustrates the screenshots when internet is disconnected and connected again. Note that internet availability is shown on the application’s screen, and as mentioned before network activity indicator animates when requests are in progress. Code snippets for parts discussed in this section are provided below. (A link to the source of the project is provided at the end of the report)

- (void)reachabilityChanged: (NSNotification* )note
{
    Reachability* curReach = [note object];
    NSParameterAssert([curReach isKindOfClass: [Reachability class]]);
    if ([curReach currentReachabilityStatus]==NotReachable)
        [self performSelector:@selector(internetDisconnected)];
    else
        [self performSelector:@selector(internetConnected)];
}

- (void)internetDisconnected
{
    NSLog(@"Damn it network's gone");
    [self performSelector:@selector(updateNetworkStatus:) withObject:@"No Internet availability"];
    [self saveRequestInfo];
}

- (void)internetConnected
{
    NSLog(@"Phew, network's back");
    [self performSelector:@selector(updateNetworkStatus:) withObject:@"Internet availabe"];
    [self initializeNetworkOperation];
}
Code snippet 5: Reachability object delegates

Figure 8: Internet connection availability

Third failure scenario: force stop by user
Two buttons, stop and resume, are provided for users, so that they can manage their download requests manually. The functionality of these buttons and what that happens in their IBAction methods is exactly the same as previous scenarios; except for the fact that unlike previous scenarios, when touching resume button, user is not asked for confirmation. This is in accordance with iOS Human Interface Guidelines 2011, which says it is not considered a good practice to take advantage of alert views to take users’ confirmation for tasks they have initiated themselves.

Conclusion
This download manager application is able to recover itself from almost any type of interruption. However, it may not be a good practice to keep track of requests similar to the way used here. Specially relying on existence of a file to determine if there are any downloads to resume, can be quite risky. Finally, as mentioned before, it would be better if downloads remained in progress in background and were saved and stopped when the application was about to terminate.
References

7 comments:

  1. hi, I am iphone developer and visiting your blog today but in the link your source code is not located . kindly give me source code .My email address is attique2010@gmail.com

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  2. Kindly give me the source code of Automating Recovery after Failure I have facing the problems regarding network falure in my iphone application .

    ReplyDelete
  3. Hello! great article!! Im trying to do something similar as proof of concept. Please can you provide your source code as the link is broken. My email is james.13.n@gmail.com Thanks. Keep the good job

    ReplyDelete
  4. HI, Very helpful blog. Iam trying something similar can't find your source code. Can you please email me the code. Me email sacha2664@gmail.com. Thanks

    ReplyDelete
  5. Hello, I love your blog. Could you please email me the code. My email is mlight.g@gmail.com. Thanks

    ReplyDelete