firebase-ios-sdk icon indicating copy to clipboard operation
firebase-ios-sdk copied to clipboard

FR: Ability to cancel Function call

Open mozeryansky opened this issue 5 years ago • 6 comments

Feature proposal

  • Firebase Component: Functions

In my app I am using function calls to download lots of data. I queue up many requests and process these. However, if my user chooses to logout while the networking is happening I would like to stop all the requests. There does not seem to be a way to cancel all running function calls.

Right now I effectively cancel the request by having the completion block call an internal completion block. When I call "cancel" I change the internal completion block to nil and call the original block with an error of "cancelled". The works for my app, but underneath the request is still occurring. (Example below for those who want my solution)

I see that Firebase Functions use a GTMSessionFetcher to make a network request using NSURLSession which creates a NSURLSessionTask, and NSURLSessionTask's have a cancel method on them.

What would it take to have a "cancelAllCalls" function? What is the current guidance on achieving this?

==== Cancelling Example ====

typealias CompletionBlock = (Result<Any, Error>) -> Void
var activeCalls = [String: CompletionBlock]()

func call(_ functionName: String, data: Any?, completion: @escaping CompletionBlock) {
    let id = UUID().uuidString
    let wrappedCompletion: CompletionBlock = { result in
        self.activeCalls[id]?(result)
        self.activeCalls[id] = nil
    }

    activeCalls[id] = completion

    functions.httpsCallable(functionName).call(data) { (result, error) in
        if let error = error {
            wrappedCompletion(.failure(error))
            return
        }
        guard let result = result else {
            wrappedCompletion(.failure(FunctionsError.invalidData))
            return
        }
        wrappedCompletion(.success(result.data))
    }
}

func cancelAllCalls() {
    for (id, result) in activeCalls {
        result(.failure(FunctionsError.cancelled))
        self.activeCalls[id] = nil
    }
}

^ My actual solution changes the completion handles to keep track of the remaining calls, and uses queues. But that's the gist of it.

mozeryansky avatar Jul 20 '20 01:07 mozeryansky

@mozeryansky Thanks for the suggestion!

it isn't possible to cancel the execution of a function on the Cloud Functions side - once the call is made, it's going to finish executing.

However, if the intent is just to ignore the response and not receive the data that is returned from the function, the suggested approach of an API addition to call GTMFetcher.stopFetching seems reasonable.

I'm not sure our team will have the bandwidth for this in the near future, but we're open to a PR.

paulb777 avatar Jul 22 '20 21:07 paulb777

@paulb777 Thanks for the info. GTMFetcher is using NSURLSessionTasks, and that class has a cancel function on it: https://developer.apple.com/documentation/foundation/nsurlsessiontask

I have already implemented the cancelling as I described for my own app, but the main issue is that the requests still are ongoing. From the Firebase/Google implementation, there's queues involved causing future calls to delay while waiting for already cancelled calls.

Could Firebase/Google take advantage of iOS to optimize this?

mozeryansky avatar Jul 22 '20 21:07 mozeryansky

Hey @mozeryansky, Joe from the Cloud Functions for Firebase team here. Thanks for the feature suggestion - this seems like it could be pretty valuable for some apps. However, I do have a few concerns about it:

  • The biggest concern is potential confusion - if we expose a 'cancelFunction' or 'cancelAllFunctions' method, it might look like a way to cancel function execution on the backend. That won't be the case - Cloud Functions doesn't offer any way to do so.
  • A secondary concern is that this is a big enough feature that we'd probably want to make it available on all of our SDKs. I'm not a IOS engineer, but the implementation you suggested looks reasonable on the surface. However, I'm not sure if the implementation for our other SDKs makes this feasible without substantial refactoring.

You also said:

I have already implemented the cancelling as I described for my own app, but the main issue is that the requests still are ongoing. From the Firebase/Google implementation, there's queues involved causing future calls to delay while waiting for already cancelled calls.

I'm not super familiar with the IOS SDK or Objective C, but I don't see any queueing logic here that would delay future calls. Any chance you could link to where you found that?

joehan avatar Jul 27 '20 17:07 joehan

I'm not sure if it would help but I see "stop"/"cancel" mentioned in the GTMSessionFetcher which Functions uses. The function the Firebase Functions use has a comment saying "...they want to be able to monitor or cancel it." https://github.com/google/gtm-session-fetcher/blob/c879a387e0ca4abcdff9e37eb0e826f7142342b1/Source/GTMSessionFetcherService.h#L113 GTMSessionFetcher has the stopFetching function which has the comment "Cancel the fetch of the request that's currently in progress. The completion handler will not be called." https://github.com/google/gtm-session-fetcher/blob/c879a387e0ca4abcdff9e37eb0e826f7142342b1/Source/GTMSessionFetcher.h#L1068 And the GTMSessionFetcherService has stopAllFetchers.

For queuing, the GTMSessionFetcherService has a property _maxRunningFetchersPerHost which determines if the fetch request should be held while other are in progress: https://github.com/google/gtm-session-fetcher/blob/c879a387e0ca4abcdff9e37eb0e826f7142342b1/Source/GTMSessionFetcherService.m#L326 And any running requests will use the resources on the operating system.

The obvious queuing I see is that I run my functions on my local machine in emulator mode, which runs the functions serially, so the 5th request won't return until requests 1-4 have completed. Even in production deployment there isn't a way to guarantee all my functions are executed as they come in, Google Cloud Functions document this behavior, so the queuing effects can be on the device or on the load balancer in the cloud.

The approach I laid out earlier works great for me. I'm not sure of a universal solution, but if I had the problem, I assumed at least someone else might as well.

If anything, what would a recommended approach to managing a queue of requests? Specifically, for downloading a data set, I chose to have many small requests instead of having fewer large requests.

mozeryansky avatar Jul 28 '20 04:07 mozeryansky

Thanks for the run through, that makes a lot more sense to me now. I think that if this was implemented, the best approach would be to expose the GTMSessionFetcher in the public files here: https://github.com/firebase/firebase-ios-sdk/tree/master/Functions/FirebaseFunctions/Public, to allow for fine grained control over how function calls are made. I'll defer to @paulb777 on this though, since he know much more about IOS and this SDK than I do.

If anything, what would a recommended approach to managing a queue of requests? Specifically, for downloading a data set, I chose to have many small requests instead of having fewer large requests.

Its hard to give a one size fits all recommendation here, unfortunately - what your app is doing with the data is going to determine what the best approach is. Some things to consider would be:

  • What is the overhead per function call? If you need to do time consuming things on each call (ie set up a DB connection), fewer function calls may improve performance.
  • What data does your app need, and when? A single large request would make sense if your app can't function without all the data - conversely, if you can show partial results, making smaller requests (and more requests as needed) can make your app more responsive. Smaller functions called as they are needed also make it easier to 'bail' (for example, if a user logs out)

If you do try a different approach, Stackdriver Functions metrics (https://cloud.google.com/functions/docs/monitoring/metrics) can help you validate whether it performs better than your current approach.

joehan avatar Jul 28 '20 20:07 joehan

Any update on this ?

quentinfasquel avatar Feb 18 '25 09:02 quentinfasquel