Using NSProcessInfo.performExpiringActivity() from a Dispatch Timer

I have static library code from which I'm attempting to use NSProcessInfo.performExpiringActivity() to ensure that our app isn't background-terminated in the middle of a relatively short (yet important) periodic background task. However, what's currently tripping me up is how NSProcessInfo.performExpiringActivity()'s block can be invoked more than once:


let queue: DispatchQueue = DispatchQueue(label: "com.my-company.ios.foo", qos: .utility)
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.setEventHandler { [weak self] in
    guard let self = self, let managedObjectContext = self.managedObjectContext else {
        timer.cancel()
        return
    }

    timer.suspend()
    ProcessInfo.processInfo.performExpiringActivity(withReason: "Performing activity") { expired in
        defer {
            timer.resume()
        }

        guard !expired else {
            return
        }

        managedObjectContext.performAndWait {
            ...
        }
    }
}


In this case, if the block is called a second time, the timer will be over-resumed and result in a crash. I could add more book-keeping variables to guard against this, but it'll certainly make the code more difficult to maintain and reason about.


Is there a more elegant and/or correct way to do this? (Links to sample code or write-ups would be appreciated!)

Replies

Is there a more elegant and/or correct way to do this?

Not that I’m aware of. I’ve found

performExpiringActivity(withReason:using:)
to be very difficult to use in practice, and so I generally prefer to use the older
UIApplication
background task API (although that API is not with its pitfalls). However, if you’re working on library code than you may not be able to rely on
UIApplication
(for example, if your\ library is loaded in an app extension).

I could add more book-keeping variables to guard against this

Even doing that is surprisingly hard. The issue is that the two block invocations — to first to run the activity and the second if it expires — are run on different queues, so this bookkeeping has to be thread safe.

How short is your operation? If it’s short in human terms (0.1 second or less), your best option is to simply ignore expiry. You seem to be doing that right now, but getting tripped up by your placement of the

resume
call. I have concerns about its placement anyway. The code snippet you posted has the resume nested within the timer’s event handler, which means that the time will only be resumed if it fires. But it can’t fire because it was never resumed.

Share and Enjoy

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

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

"The code snippet you posted has the resume nested within the timer’s event handler, which means that the time will only be resumed if it fires. But it can’t fire because it was never resumed."

Ah, that was a typo actually. I've corrected it now to include the timer.suspend() before the activity block (which still has the problem I've described above).


What I really want here is for the timer's logic, including the activity block, to not be re-entrant. Overall, the timer should not fire again until the logic in the activity block has completed or expired.