Swift iOS -DispatchWorkItem is still running even though it's getting Cancelled and Set to Nil - ios

I use GCD's DispatchWorkItem to keep track of my data that's being sent to firebase.
The first thing I do is declare 2 class properties of type DispatchWorkItem and then when I'm ready to send the data to firebase I initialize them with values.
The first property is named errorTask. When initialized it cancels the firebaseTask and sets it to nil then prints "errorTask fired". It has a DispatchAsync Timer that will call it in 0.0000000001 seconds if the errorTask isn't cancelled before then.
The second property is named firebaseTask. When initialized it contains a function that sends the data to firebase. If the firebase callback is successful then errorTask is cancelled and set to nil and then a print statement "firebase callback was reached" prints. I also check to see if the firebaseTask was cancelled.
The problem is the code inside the errorTask always runs before the firebaseTask callback is reached. The errorTask code cancels the firebaseTask and sets it to nil but for some reason the firebaseTask still runs. I can't figure out why?
The print statements support the fact that the errorTask runs first because
"errorTask fired" always gets printed before "firebase callback was reached".
How come the firebaseTask isn't getting cancelled and set to nil even though the errorTask makes those things happen?
Inside my actual app what happens is if a user is sending some data to Firebase an activity indicator appears. Once the firebase callback is reached then the activity indicator is dismissed and an alert is shown to the user saying it was successful. However if the activity indicator doesn't have a timer on it and the callback is never reached then it will spin forever. The DispatchAsyc after has a timer set for 15 secs and if the callback isn't reached an error label would show. 9 out of 10 times it always works .
send data to FB
show activity indicator
callback reached so cancel errorTask, set it to nil, and dismiss activity indicator
show success alert.
But every once in while
it would take longer then 15 secs
firebaseTask is cancelled and set to nil, and the activity indicator would get dismissed
the error label would show
the success alert would still appear
The errorTask code block dismisses the actiInd, shows the errorLabel, and cancels the firebaseTask and sets it to nil. Once the firebaseTask is cancelled and set to nil I assumed everything inside of it would stop also because the callback was never reached. This may be the cause of my confusion. It seems as if even though the firebaseTask is cancelled and set to nil, someRef?.updateChildValues(... is somehow still running and I need to cancel that also.
My code:
var errorTask:DispatchWorkItem?
var firebaseTask:DispatchWorkItem?
#IBAction func buttonPush(_ sender: UIButton) {
// 1. initialize the errorTask to cancel the firebaseTask and set it to nil
errorTask = DispatchWorkItem{ [weak self] in
self?.firebaseTask?.cancel()
self?.firebaseTask = nil
print("errorTask fired")
// present alert that there is a problem
}
// 2. if the errorTask isn't cancelled in 0.0000000001 seconds then run the code inside of it
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0000000001, execute: self.errorTask!)
// 3. initialize the firebaseTask with the function to send the data to firebase
firebaseTask = DispatchWorkItem{ [weak self] in
// 4. Check to see the if firebaseTask was cancelled and if it wasn't then run the code
if self?.firebaseTask?.isCancelled != true{
self?.sendDataToFirebase()
}
// I also tried it WITHOUT using "if firebaseTask?.isCancelled... but the same thing happens
}
// 5. immediately perform the firebaseTask
firebaseTask?.perform()
}
func sendDataToFirebase(){
let someRef = Database.database().reference().child("someRef")
someRef?.updateChildValues(myDict(), withCompletionBlock: {
(error, ref) in
// 6. if the callback to firebase is successful then cancel the errorTask and set it to nil
self.errorTask?.cancel()
self.errorTask? = nil
print("firebase callback was reached")
})
}

This cancel routine is not doing what I suspect you think it is. When you cancel a DispatchWorkItem, it performs no preemptive cancellation. It certainly has no bearing on the updateChildValues call. All it does is perform a thread-safe setting of the isCancelled property, which if you were manually iterating through a loop, you could periodically check and exit prematurely if you see that the task was canceled.
As a result, the checking of isCancelled at the start of the task isn't terribly useful pattern, because if the task has not yet been created, there is nothing to cancel. Or if the task has been created and added to a queue, and canceled before the queue had a chance to start, it will obviously just be canceled but never started, you'll never get to your isCancelled test. And if the task has started, it's likely gotten past the isCancelled test before cancel was called.
Bottom line, attempts to time the cancel request so that they are received precisely after the task has started but before it has gotten to the isCancelled test is going to be an exercise in futility. You have a race that will be almost impossible to time perfectly. Besides, even if you did happen to time this perfectly, this merely demonstrates how ineffective this whole process is (only 1 in a million cancel requests will do what you intended).
Generally, if you had asynchronous task that you wanted to cancel, you'd wrap it in an asynchronous custom Operation subclass, and implement a cancel method that stops the underlying task. Operation queues simply offer more graceful patterns for canceling asynchronous tasks than dispatch queues do. But all of this presumes that the underlying asynchronous task offers a mechanism for canceling it and I don't know if Firebase even offers a meaningful mechanism to do that. I certainly haven't seen it contemplated in any of their examples. So all of this may be moot.
I'd suggest you step away from the specific code pattern in your question and describe what you are trying to accomplish. Let's not dwell on your particular attempted solution to your broader problem, but rather let's understand what the broader goal is, and then we can talk about how to tackle that.
As an aside, there are other technical issues in your example.
Specifically, I'm assuming you're running this on the main queue. So task.perform() runs it on the current queue immediately. But your DispatchQueue.main.asyncAfter(...) can only be run when whatever is running on the main queue is done. So, even though you specified a delay of 0.0000000001 seconds, it actually won't run until the main queue is available (namely, after your perform is done running on the main queue and you're well past the isCancelled test).
If you want to test this race between running the task and canceling the task, you need to perform the cancel on a different thread. For example, you could try:
weak var task: DispatchWorkItem?
let item = DispatchWorkItem {
if (task?.isCancelled ?? true) {
print("canceled")
} else {
print("not canceled in time")
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.00001) {
task?.cancel()
}
task = item
DispatchQueue.main.async {
item.perform()
}
Now you can play with various delays and see the different behavior between a delay of 0.1 seconds and one of 0.0000000001 seconds. And you'll want to make sure the app has reached quiescence before you try this test (e.g. do it on a button press event, not in viewDidLoad).
But again, this will merely illustrate the futility of the whole exercise. You're going to have a really hard time catching the task between the time it started and before it checked the isCancelled property. If you really want to manifest the cancel logic in some repeatable manner, we're going to have to artificially make this happen:
weak var task: DispatchWorkItem?
let queue = DispatchQueue(label: "com.domain.app.queue") // create a queue for our test, as we never want to block the main thread
let semaphore = DispatchSemaphore(value: 0)
let item = DispatchWorkItem {
// You'd never do this in a real app, but let's introduce a delay
// long enough to catch the `cancel` between the time the task started.
//
// You could sleep for some interval, or we can introduce a semphore
// to have it not proceed until we send a signal.
print("starting")
semaphore.wait() // wait for a signal before proceeding
// now let's test if it is cancelled or not
if (task?.isCancelled ?? true) {
print("canceled")
} else {
print("not canceled in time")
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
task?.cancel()
semaphore.signal()
}
task = item
queue.async {
item.perform()
}
Now, you'd never do this, but it just illustrates that isCancelled does work.
Frankly, you'd never use isCancelled like this. You would generally use the isCancelled process if doing some long process where you can periodically check the isCancelled status and exit if it is true. But that's not the case in your situation.
The conclusion of all of this is that checking isCancelled at the start of a task is unlikely to ever achieve what you had hoped for.

Related

Timer.ScheduledTimer not initialising quickly enough if started on main thread

In the accepted answer for Timer.scheduledTimer not firing, it is emphasised to start the timer on the main thread to ensure that it fires. However, if I do that then I often end up with the timer being slow to initialise, and therefore failing in its purpose as a debouncer. Just wondering if there is something I am doing wrong, or a better way of doing this.
My problem (pseudocode at the bottom):
I use a JWT to authenticate my server calls, and I check this locally to see if it's expired before submitting it. However, I don't want several network calls to notice the expired JWT all at once and submit several refresh requests, so I use a semaphore to ensure only one call at a time is checking/renewing the JWT. I also use a dispatchGroup to delay the original network call until after the checking/renewing is done. However, if the refresh fails I want to avoid all the queued calls then trying it again. I don't want to block all refresh calls forever more with a boolean, so I thought I would create a scheduledTimer to block it. However, if I create it on the main thread, there's a delay before it's created and the released network calls submit a few more refresh attempts before they're blocked.
Questions
Should I just create the timer on the local thread to ensure there's no delay (I presume the main thread is occupied with some UI tasks which is why the timer doesn't get created instantly?)
More generally, is there a better way of doing this? I suspect there is - I tried playing with adding items to a queue, and then cancelling them, but then I began getting worried about creating work items with out of date values of functions, and capturing things in closures etc (it was a while ago, I can't remember the details), so I went with my current bodge.
This might all be easier if I was using await/async, but our app supports all the way back to iOS12, so I'm stuck with nests of completion handlers.
Hopefully this pseudocode is accurate enough to be helpful!
private static let requestQueue: DispatchQueue = DispatchQueue(label: "requestQueue", qos: .userInteractive, attributes: .concurrent)
public static let jwtValidityCheckSemaphore: DispatchSemaphore = DispatchSemaphore(value: 1)
private static var uglyHackTimer: Timer?
#objc private class func clearUglyHackTimer(){
uglyHackTimer?.invalidate()
uglyHackTimer = nil
}
class func myNetworkCall(for: myPurposes){
let group = DispatchGroup()
jwtValidityCheckSemaphore.wait()
if (uglyHackTimer?.isValid ?? false){
jwtValidityCheckSemaphore.signal()
return
}
group.enter()
if jwtIsInvalid(){
refreshJWT(){success in
if !success{
DispatchQueue.main.async{
self.uglyHackTimer = Timer.scheduledTimer(timeInterval: TimeInterval(2), target: self, selector: #selector(clearUglyHackTimer), userInfo: nil, repeats: false)
}
}
group.leave()
jwtValidityCheckSemaphore.signal()
}
}else{
group.leave()
jwtValidityCheckSemaphore.signal()
}
// Make the original network call
newNetworkRequest = DispatchWorkItem{
// Blah, blah
}
group.notify(queue: requestQueue, work: newNetworkRequest)
}

beginBackgroundTask expirationHandler never called

I'm trying to complete a task in the background using UIApplication.shared.beginBackgroundTask but for some reason the expirationHandler is never called. The task I'm trying to complete is a video export from photo library but sometimes the export cannot be completed in time while the user is using the app in the foreground.
This is the code I'm using :
func applicationDidEnterBackground(_ application: UIApplication) {
if backgroundTask == .invalid && UploadQueue.instance.hasMoreWork() {
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "ExportQueue") {
NSLog("DriveLog - System is requesting end. Still more work to do ...")
self.endBackgroundTask()
}
print("Invalid? \(backgroundTask == .invalid)")
NSLog("DriveLog - Starting background task: %i", backgroundTask.rawValue)
}
}
func endBackgroundTask() {
NSLog("DriveLog - End called")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
I'm also calling :
(UIApplication.shared.delegate as! AppDelegate).endBackgroundTask()
during my task if I finish it earlier.
However I never see my expirationHandler being called in the log.
I have also tried putting beginBackgroundTask when starting the task in foreground but I get a warning message about task expiration while being in foreground.
You have not understood what the expiration handler is. It is called only if your time expires. Hence the name.
As soon as you call begin, start your task in the next line (not in the expiration handler). And when you are finished, call end.
You thus need to end the background task in two places: in the expiration handler, and outside it after actually performing your task.
It is very important to call end in both places, because if you fail to do so, the system will decide that you are a bad citizen and will never grant you any extra background time at all.
So, this is the diagram of the flow you need to construct:
Also note that this has nothing to do with UIBackgroundModes. That's a totally different mechanism.
matt's answer covers everything. I'm just going to try to give the same answer in different words because your edit suggests that matt's answer wasn't clear to you. (Read it again, though, it really does cover everything I'm going to say here, just in different words.)
You should not call beginBackgroundTask in applicationDidEnterBackground. You call it when you start whatever task you want time for. In your example that's going to be somewhere inside of UploadQueue. You don't call beginBackgroundTask when going into the background. You call it when you're starting a task that you would like to finish even if you go into the background.
Background tasks generally do not belong to the UIAppDelegate. They belong to the thing that creates the task (in your case: UploadQueue). You can create all the background tasks you want. They cost almost nothing. It's not just one "I want background" at the app level. Read matt's flow chart closely.
It's unclear from your question why you expect the expiration handler to be called. Do you expect your task to task to take so long that the OS forces you to stop it? That's what the expiration handler is for. If you've built your system correctly, it should rarely be called. Your task should end long before it's expired.
For full docs on how to do this, see Extending Your App's Background Execution Time. In particular note the caution:
Don’t wait until your app moves to the background to call the beginBackgroundTask(withName:expirationHandler:) method. Call the method before performing any long-running task.

Rightness of waiting for a network call to complete - iOS

I'm curious on the user experience for an user, while they wait for a network call to complete over cancelling the existing non deterministic request. Let me add more context. I'm prefetching data for my app that is later used. When the user hits the button, we use this data to load a screen. Instead of showing a spinner to the user and waiting on the network call to complete, we wanted to give them a better user experience.
class TestInteractor {
var currentTask: HTTPTask?
var content: SomeContent?
func getData(_ id: String, completion: Result<SomeContent, Error>) {
currentTask = URLSession.shared().dataTask(with: request) {
// check for no error
// set content here
}
}
var hasContent: Bool {
return content != nil
}
}
Here is the issue, if the prefetch is still in process (due to a bad network) should I let the user wait until this call completes or just cancel the task and restart a new call.
Canceling an existing network call can be implemented as below:
func getData(_ id: String) {
if let task = currentTask {
task.cancel()
currentTask = nil
}
// Continue with a new network call
}
Or should I add a new property to the TestInteractor and check if the network is still in progress and wait?
var isNetworkCallInProgress: Bool {
return currentTask?.state == running
}
There could be numerous reasons why a network request hasn’t completed yet; your server may be a bit overwhelmed; the client’s network speed may be a bit slow. It may be a waste to abort the work and start over. And whose to say that restarting the task is going to change any current impediment.
I’d say wait on the running task until it completes. If the pre-fetch completes before we need it, great, the pre-fetch saved time. But if it’s not yet done by the time we need it, if you let it finish, that’ll still save time rather than restarting it (the restarted task isn’t gonna magically be faster than the previous one just because we restarted it) so the pre-fetch was useful in this case too. So by allowing the request to complete, you’re maximizing the utility of the pre-fetch mechanism. Plus, if you choose to restart a task because pre-fetch couldn’t complete in time, what if your average user is actually faster than your average serving time for that request? Lol who knows, you might end up doubling your server load for the average case. Better that you have a design that is decoupled from things like that.
First, your app has a network activity indicator. Make a counter of how many network tasks you started, how many have finished, and turn the network activity indicator on or off when the count changes from 0 to 1 or from 1 to 0. That shows network activity very nicely.
Second, in your data model you will have items with correct data, items that are being loaded, and items that are not being loaded. Write code to display each kind of item. When a network request finishes, it updates your data model, and that redraws the corresponding item.
You can give the user a choice, you could add a refresh button to reset the call or let them wait for it.
If you want to ask them if it's working, you could just push an alert asking them if they want to refresh the call while running the prefetch in the background.
let alert = UIAlertController(title: "Message Title", message: "body text", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "button", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
This is the code for an alert. I would personally check and see if the process is taking long and then push out the alert asking the to either refresh or wait.

What is the purpose of Semaphore.wait(timeout: .now())?

Looking at some Apple code sample, I found this:
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
// wait() is used to drop new notifications if old ones are still processing, to avoid queueing up a bunch of stale data.
if metadataObjectsOverlayLayersDrawingSemaphore.wait(timeout: .now()) == .success {
DispatchQueue.main.async {
// Some processing...
self.metadataObjectsOverlayLayersDrawingSemaphore.signal()
}
}
}
Context of the code: This is the delegate method when using video capture to detect a QR code (or any other code).
Because it is triggered many times per second (if the camera stays on the same QR code), some kind of timeout is needed.
But how does DispatchSemaphore.wait(timeout: .now()) work? And why check if it is .success?
The purpose is what the comment says:
wait() is used to drop new notifications if old ones are still processing, to avoid queueing up a bunch of stale data.
and it works as follows:
The semaphore is created with a value of one.
When metadataOutput is called the first time, wait(timeout: .now())
succeeds and decrements the value of the semaphore to zero.
The processing of the data begins.
If metadataOutput is called again before the processing has
completed, the semaphore still has a value of zero.
Then wait(timeout:) would wait for the semaphore to become
positive again, but since the timeout value is now(), it fails
immediately and returns .timedOut.
The effect is that the incoming data is ignored, the metadataOutput
callback method returns immediately.
When the data processing on the has completed,
the semaphore is signaled, which increases the value to one.
As a consequence, the next time the callback is called,
waiting for the semaphore will succeed and the data is processed again.
So in short:
wait(timeout: .now()) returns .success if a
previously submitted block has signaled completion, in that case
a new block is submitted for processing the incoming data.
wait(timeout: .now()) returns .timedOut if a previously
submitted block is still running, in that case the incoming data
is ignored.

Force early call to background task expiration handler on iOS devices for testing

I am testing and debugging the expiration block in - beginBackgroundTaskWithExpirationHandler:.
Is there a way to force the Block call so that it happens quicker, instead of waiting for about 10 minutes each time I need to debug it?
I am not interested in debugging the actual code in the block, rather I am interested in the sequence of calls and the backtrace, etc.; that's why I need the callback itself to happen, but 10 minutes each time is too long!
Emulating the expiration of the background task time:
Start your background task on controller viewDidLoad and Schedule a Timer that prints out the backgroundTimeRemaining every 1 second.
Send your app to background.
Trigger the action of your background task when the backgroundTimeRemaining is less than X seconds.
X can be known by testing when the expiration handler is triggered. because the backgroundTimeRemaining is not guaranteed to be accurate.
Code:
UIApplication.shared.beginBackgroundTask(withName: "Submit Order Background Task") { [weak self] in
// Your logic to handle the expiration.
}
// To submit order 1 time!
var submitted = false
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
print("background task timer: \(UIApplication.shared.backgroundTimeRemaining)")
if UIApplication.shared.backgroundTimeRemaining < 2 && !submitted {
self.submitOrder()
submitted = true
}
}
If you are looking to force the system to call the expiration block, I don't think that can be done. However, I would suggest that you isolate your background task block, then call it using a NSTimer or
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
At least thats how I would do it.
Meaning, that you would call the code in your expiration handler directly using a NSTimer (implied or explicitly). So, if in your expiration handler you called a method that handled the expiration. Then, you would set a timer for that method to be called outside of the "background task" lifecycle, but you would be only simulating it.

Resources