8 Replies
      Latest reply: Oct 12, 2016 2:15 AM by eskimo RSS
      steve@root Level 1 Level 1 (0 points)

        What is the best practice for persisting state associated with background Session Up/Download Tasks across app termination/relaunch?

         

        For example, background upload Data task isn't supported. So Data content has to be copied to a temp file for background upload. When the upload completes, the temp file should be deleted. So the temp file URL has to be "remembered".

         

        If iOS terminates the app (e.g. memory pressure) or the app crashes, when the upload completes the app is relaunched in background.

         

        Task state info can't be maintained by the URLSession object because it is created anew when the app delegate receives 'handleEventsForBackgroundURLSession'.

         

        Nor can I store state in the Session Task as an associated object because the Task is also "new" in the sense that when notified the Task has completed, it is not the same object the app created.

         

        Similarly for a Download Task, Data accumulates as received. How best to "save" incremental Data so that if terminated and relaunched, all Data received thus far is available?

         

        The obvious answer is UserDefaults, but that seems less than optimal as a temporary backing store for accumulating

        download Data.

         

        If I overlooked this in the documentation or the Dev forum archive, a RTFM gratefully accepted.

         

        Cheers...

        • Re: Background Session Task state persistence
          eskimo Apple Staff Apple Staff (7,190 points)

          You're right that this is a challenge.  It's one I've dealt with myself but, alas, I have not had time to publish my code as sample code.

          Task state info can't be maintained by the URLSession object because it is created anew when the app delegate receives 'handleEventsForBackgroundURLSession'.

          Nor can I store state in the Session Task as an associated object because the Task is also "new" in the sense that when notified the Task has completed, it is not the same object the app created.

          While the task object is re-created, much of the data in that object is the same as in the previous object.  The data items of interest include:

          • the URL itself (via task.originalRequest.URL)

          • the task identifier (via task.taskIdentifier)

          You can use these to match the task object to your persistent private state.

          Another much-less-obvious option is to store a custom property on the request via +[NSURLProtocol setProperty:forKey:inRequest:].  I used this to associate a UUID with the task, and I used that UUID as the key to match against my database.

          And to be clear, my database wasn't anything complex, but rather a layout of files on the file system.  My brain has paged out the exact details but the basic gist of things was:

          • There's a directory, named by the UUID, that holds all the transfer state.

          • Inside the directory there's a property list file that holds immutable data about the transfer.  I write this file when I create the transfer and I expect it to always be valid from then on.  If it's not I nuke the transfer entirely.  This file holds the critical data needed to restart the transfer from scratch if necessary.

          • There's a separate property list file that holds mutable state about the transfer.  This gets written to frequently, so there's a greater chance that it might be corrupt (because, say, I crash while updating the file).  However, if this file is corrupt I can re-create the transfer based on the immutable data.

          • For uploads, the directory contains a copy of the data being uploaded.  If the file I'm uploading is immutable, I use a hard link to avoid making a copy.

          IMPORTANT When you try to reconnect with your NSURLSession background session, there are four potential states, of which you have to deal with three:

          • no NSURLSession task, no private state record — This is the one state you can ignore.

          • NSURLSession task but no private state record — You've lost track of the transfer but NSURLSession still knows about it.  In this case it's probably best to just cancel the task because you don't have any of your private state for it.

          • private state record but no NSURLSession task — NSURLSession has lost track of the transfer but you still know about it.  In this case it's probably best to recreate the NSURLSession task and continue with the transfer.

          • NSURLSession task and private state record — This is the expected case.

          Share and Enjoy

          Quinn "The Eskimo!"
          Apple Developer Relations, Developer Technical Support, Core OS/Hardware
          let myEmail = "eskimo" + "1" + "@apple.com"

            • Re: Background Session Task state persistence
              steve@root Level 1 Level 1 (0 points)

              Thanks Quinn, I appreciate the guidance. Just what I was looking for. Cheers…

              • Re: Background Session Task state persistence
                steve@root Level 1 Level 1 (0 points)

                Quinn, perhaps I misunderstood. You wrote:

                "store a custom property on the request via +[NSURLProtocol setProperty:forKey:inRequest:].  I used this to associate a UUID with the task, and I used that UUID as the key to match against my database."

                 

                In my experimentation, if I assign a custom property on a background Upload Task's originalRequest, initiate the upload, then when the Upload Task completes, I can get that property value from the task.originalRequest, just as you suggested.

                 

                But if after initiating the upload, I abort() the app, and when relaunched in background, reconnect the session, etc. then when the Upload Task completes, the property is absent from the request. The (new instance of the) Upload Task has the same identifier, and its originalRequest has the same URL, just as you describe. Just no custom properties with which I can persist state information across app termination/relaunch.

                 

                Have I misinterpreted your advice? Cheers…

                  • Re: Background Session Task state persistence
                    eskimo Apple Staff Apple Staff (7,190 points)

                    You're interpreting my approach correctly, although I'm not 100% sure what you mean by original request.  I presume you're doing that on a mutable request before you pass it to -downloadTaskWithRequest:.  If so, that's exactly what I did.

                    Here's my code to set it:

                    request = [[NSMutableURLRequest alloc] initWithURL:download.targetURL];
                    [NSURLProtocol setProperty:[download.downloadUUID UUIDString] forKey:kDownloadUUIDStrPropertyKey inRequest:request];
                    result = [self.session downloadTaskWithRequest:request];
                    

                    and here's the Quinn-level-of-paranoia code to get it:

                    downloadUUIDStr = [NSURLProtocol propertyForKey:kDownloadUUIDStrPropertyKey inRequest:task.originalRequest];
                    if (downloadUUIDStr == nil) {
                        assert(NO);
                    } else if ( ! [downloadUUIDStr isKindOfClass:[NSString class]] ) {
                        assert(NO);
                    } else {
                        downloadUUID = [[NSUUID alloc] initWithUUIDString:downloadUUIDStr];
                        if (downloadUUID == nil) {
                            assert(NO);
                        } else {
                            ...
                        }
                    }
                    

                    where:

                    • download is an object I use to track the download

                    • kDownloadUUIDStrPropertyKey is my custom key

                    The one odd thing is that I store the property as a string rather than an NSUUID.  I saw some weird behaviour with NSUUID when I first wrote this code and, at the time, I needed a quick fix so I switched to a string.  Alas, I've never returned to this issue to figure out what the real story with NSUUID is.

                    Share and Enjoy

                    Quinn "The Eskimo!"
                    Apple Developer Relations, Developer Technical Support, Core OS/Hardware
                    let myEmail = "eskimo" + "1" + "@apple.com"

                      • Re: Background Session Task state persistence
                        ThyncKurt Level 1 Level 1 (0 points)

                        Because of the way NSUUID's are wrappers of CFUUID's in one of the most bizzarre nonsense "toll free bridging" that I have run across in Foundation, NSUUID's cannot be used as keys, stored in another containers for any extended period of time, or even subclassed.  You always must use the string representation as the key.  In fact the longer you hold on to an NSUUID, the more likely you are to discover that it's underlying representation has been deallocated from under you and it can no longer return the value that it returned shortly after alloc/init.  Fun stuff.

                          • Re: Background Session Task state persistence
                            eskimo Apple Staff Apple Staff (7,190 points)

                            Because of the way NSUUID's are wrappers of CFUUID's in one of the most bizzarre nonsense "toll free bridging" that I have run across in Foundation …

                            NSUUID is simply not toll-free bridged to CFUUID; the comments at the top of the header make that very clear.

                            NSUUID's cannot be used as keys, stored in another containers for any extended period of time, or even subclassed.

                            Subclassing is probably a bad idea, as it is for most Foundation primitive types, but the others should be fine.  I played around with NSUUID a bit and didn’t have any problems with using it as a dictionary key or value.  Perhaps you could post a concrete example of the problems you’re having?

                            Share and Enjoy

                            Quinn "The Eskimo!"
                            Apple Developer Relations, Developer Technical Support, Core OS/Hardware
                            let myEmail = "eskimo" + "1" + "@apple.com"

                      • Re: Background Session Task state persistence
                        mattie Level 1 Level 1 (0 points)

                        Holy moly this is an incredibly useful technique! It should really be mentioned in offical documenation. Thank you so much for sharing this!