Search code examples
iosmultithreadingweb-servicesrx-swiftmoya

iOS Moya Web Services: make one call wait for all others to finish, then block new calls


In my app I am using Moya and RxSwift to make my web service calls. The calls are async and can be triggered by user interaction as well as remote notifications when new data is available.

Each web service calls needs a authentification taken in its headers. When the user changes the password, the token is re-generated and returned by the change password web service call.

Now it can happen that while the user changes the password, a remote notification comes and causes another web service call. Depending on the servers load and how the system is handling the different threads it could in theory happen that the call is made before the new token is retrieved by the other call but after the server already invalidated the old token. The result is a HTTP 401 unathorized error.

I would like to prevent that but I am not sure what the best approach to that is or if I have some wrong thinking in my concept.

I found this page which talks about locks, mutex and semaphores: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html

It seems that I might should use a "Read-write lock" as such:

  • The change password call is the "writer"
  • All other calls are the "readers"
  • When a call to reload data is made, either by the user or caused by a remote notification, the reader count is incremented on the lock
  • When the user changes the password, the writer count is incremented on the lock and new readers are blocked from starting
  • The change password call waits for all other "read" calls to finish
  • The change password call changes the password, updates the token and finally decrements the lock and releases it
  • The suspended readers can now continue to run and start incrementing the readers count and reload the data.

Is this correct so far? Then next big question is: is there a better approach? And before I start changing all my web service calls: is there a build in mechanism for this in Moya or RxSwift?


Solution

  • I'd like to share some thoughts about build in mechanisms in RxSwift. I think there are some ways to achieve the desired behavior.

    The code below is just a theory and hasn't been tested. Please, don't cmd+c cmd+v this code. Its purpose is to show the simplified version of potential solution.

    Lets say we have:

    var activityIndicator = ActivityIndicator()
    var relayToken = BehaviorRelay<String?>(value: nil)
    

    where ActivityIndicator is this struct which helps to catch the activity of multiple Observables.

    In theory, the request method will look something like this:

    func request<D, P, R>(data: D, parameters: @escaping (D, String) -> P, response: @escaping (P) -> Observable<R>) -> Observable<R> {
        return Observable
            .just(data)
            .flatMap({ (data: D) -> Observable<R> in
                return relayToken
                    .asObservable()
                    .filterNil()
                    .take(1)
                    .map({ parameters(data, $0) })
                    .flatMap({ (parameters: P) -> Observable<R> in
                        return activityIndicator.trackActivity(response(parameters))
                    })
            })
    }
    

    where:

    data: D - the initial request parameters

    parameters: @escaping (D, String) -> P - the closure which transforms initial parameters with token to full request parameters.

    response: @escaping (P) -> Observable<R> - the closure which transforms full parameters to a proper request Observable. It's a place where Moya or some other mechanism is used.

    This function waits for a single valid token signal and when a proper token is received - it transforms it to a response Observable, which is also tracked by activityIndicator. Such tracking is required to know when all "read" calls are finished.

    As a result - each request activity is tracked and any request starts only when a valid token is received.

    The 2nd important thing - change the token only when there are no active requests:

    func update(token: String?) {
        _ = Observable
            .just(token)
            .flatMap({ (token: String?) -> Observable<String?> in
                return activityIndicator
                    .asObservable()
                    .filter({ $0 == false })
                    .take(1)
                    .map({ _ in token })
            })
            .bind(to: relayToken)
    }
    

    So, whenever you decide to change the token - you apply its change through this function. It observes the activity of all request and when they all finish - the change will be applied.

    Hope it helps, ask questions if required.

    Edit 1

    "PATCH changePassword" might not be one of the normal requests and its activity might not be tracked with activityIndicator.

    1. Set relayToken = nil when you need to change the password (all future normal requests are going to wait for a proper token since this step)
    2. Wait for already started requests to finish (activityIndicator will help again)
    3. Send the "PATCH changePassword" request to change the password / token
    4. Write new token relayToken = some
    5. All paused request will get a new token and start execution automatically
    6. Back to step 1 when required

    So, the server will only invalidate the token after all already started requests are completed.

    My solution blocks all new requests in:

    relayToken.asObservable().filterNil().take(1)
    

    It means, that while token is nil - wait. When not nil - proceed only once.