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 .
But every once in while
firebaseTask
is cancelled and set to nil, and the activity indicator would get dismissedThe 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")
})
}
When you cancel a DispatchWorkItem
, it does not perform preemptive cancelation. 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: If you cancel a work item that has not started, it simply never starts and you will never reach this isCancelled
test. And if the task has started, it has 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 of limited utility.
Historically, if you had asynchronous task that you wanted to cancel, you might wrap it in an asynchronous custom Operation
subclass, which offers more elegant cancelation support. In this scenario, one might implement a cancel
method that stops the underlying work. 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.
Even better, nowadays we would probably reach for Swift concurrency, which offers first-class cancelation support. It is still a cooperative cancelation pattern, but many async
-await
API offer native cancelation support. And with structured concurrency, especially, it offers elegant propagation of cancelation from parent tasks to child tasks.
I might 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.