Background Session Task state persistence

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...

Accepted Reply

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 **** 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"

Replies

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 **** 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"

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

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…

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"

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.

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"

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

It should really be mentioned in offical documenation.

You know what to do (-:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks, Quinn, for this answer.


In NSURLProtocol's interface,


+(void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request


`value`
is of type
id
but presumably this must be a serializable value i.e.,
NSString
in your example.


Is any object conforming to

NSCoding
or supported by
NSPropertyListSerialization
accepted? Perhaps the interface or documentation could be updated to reflect what's supported?
value
is of type
id
but presumably this must be a serializable value

For background sessions, yes. I was going to suggest that you file a bug requesting that we fix the headers to make this clear but then I realised that

id
is correct for standard sessions.

The most general thing you should expect to work is an

NSSecureCoding
that only references Foundation classes. Anything more general than that definitely won’t work because the value needs to be passed to the background session daemon and that doesn’t have access to your classes. Personally I’d recommend that you stick to something simple. For example, a property list strikes a good balance between simplicity and generality.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"