Promisekit: Polling with 'after(interval:).then(execute:)'

Created on 5 May 2017  Β·  11Comments  Β·  Source: mxcl/PromiseKit

  • PromiseKit version: 4.2.0
  • Xcode version: 8.3.2

I'm new to the library but it's been great so far!

Right now I'm trying to implement a polling system which continues to make API calls until a flag comes back as "finished". It's similar to how "retry" is implemented in the PromiseKit docs but the difference is that this continues while a flag is turned off, and stops when it's turned on. Not until it does not find an error, like in the retry example.

I had luck implementing the solution you proposed here #387 and updating the syntax a little bit for the new Swift & PromiseKit versions. Here is my implementation:

enum PromiseError: Error {
    case Timeout
}

func poll(signal: @escaping () -> Bool, interval: TimeInterval, timeout: TimeInterval) -> Promise<Void> {
    func pollRecursive() -> Promise<Void> {
        return signal() ? Promise(value: ()) : after(interval: interval).then(execute: pollRecursive)
    }

    return race(after(interval: timeout).then { throw PromiseError.Timeout }, pollRecursive())
}

func randomSignal() -> Bool {
    let r = arc4random_uniform(10) == 0
    print(r ? "Complete" : "Not complete");
    return r
}

poll(signal: randomSignal, interval: 1, timeout: 2).then {
    print("done")
}.catch { error in
    print("timeout error")
}

The results I get in the playground console are these:

Not complete
Not complete
Not complete
timeout error -- Should have stopped here
Not complete -- But continued calling pollRecursive() and signal() functions
Not complete
Not complete
Not complete
Not complete
Complete -- Until signal was true

I thought the race function should return the first promise that finished, and the timeout should abort the promise, no? Am I missing something?

Sorry for the dumb question.

Most helpful comment

So promises start as soon as they're initialized, or until .then is called?

Promises make no decision whatsoever about when the underlying tasks they represent start. See the FAQ.

This is the final code. Does it seem reasonable?

Not really. You are doing three catch handlers, one for each promise. You may as well just use completion blocks if you are going to use promises like that. One of the primary reasons to use promises is so you can consolidate error handling at a lower layer and thus handle error conditions for a wider amount of state conditions.

I was thinking, knowing that both poll promise chains have finished might be useful for UI purposes, like stopping an activity indicator for example.

I could use join or when, so the promise completes when all of them complete, that way I know when they are all finished.

BUT then I'm back to square one, because I also want to know when each one is finished, I don't need to wait for all of them to complete to update UI. That's why I followed your example of creating separate promise chains each one with their own .then handler.

Don’t forget, promises are composable:

firstly {
    searchService.search(searchQuery)
}.then { searchResults -> Void in

    let p1 = busPoll().then {
        //…    
    }

    let p2 = planePoll.then {
        //…
    }

    return when(fulfilled: p1, p2)
}

I think I need to somehow merge the POLL implementation, with the RETRY implementation in the docs, and implement some form of RECOVER. But the recover function returns a "default" value of some sort. I just really want to tell the promise chain to continue, and ignore the error.

The only logical way recover could work is if it returns a default value, for example:

foo().then {
    return 1
}.recover {
    // ignore error
}.then { x in
   // what is x if recover returns nothing? This is an impossible situation.
}

Swift itself cannot compile this scenario, which is a reason promises in typed languages work so well.

If you find yourself with no logical value to return then you should probably be using Void promises.

All 11 comments

Indeed, there's two separate chains, which are raced, thus both chains will complete. To prevent the right chain completing if the left chain times out you must incorporate the timeout into the right chain also:

enum PromiseError: Error {
    case Timeout
}

func poll(signal: @escaping () -> Bool, interval: TimeInterval, timeout: TimeInterval) -> Promise<Void> {
    let timeout = after(interval: timeout).then { throw PromiseError.Timeout }

    func pollRecursive() -> Promise<Void> {
        guard timeout.isPending else { return timeout }
        return signal() ? Promise(value: ()) : after(interval: interval).then(execute: pollRecursive)
    }

    return race(after(timeout, pollRecursive())
}

poll(signal: randomSignal, interval: 1, timeout: 2).then {
    print("done")
}.catch { error in
    print("error")
}

func randomSignal() -> Bool {
    let r = arc4random_uniform(4) == 0
    print(r ? "Complete" : "Not complete");
    return r
}

race is still necessary.

Thanks @mxcl ! This makes total sense. Made it work with a "max attempts" instead of a timeout, but will implement it with a timeout now that I know how πŸ‘

Now I'm completely stuck again :( haha

firstly {
    searchService.search(searchQuery)
}.then { searchResults -> Promise<PlanePollResults> in

    poll(interval: 1, maxAttempts: 10, body: { () -> Promise<PlanePollResults> in
        self.searchService.pollPlane(searchId: searchResults.info.id)
    }, signal: { (planePollResults) -> Bool in
        planePollResults.state == .finished
    })

}

I first need to do a search which gives me an ID so I can start doing polling. Doing polling for "plane flights" works perfectly as in the code above. But I also need to simultaneously/concurrently do polling for "bus trips". I'm thinking of something like so:

firstly {
    searchService.search(searchQuery)
}.then { searchResults -> Promise<PlanePollResults> in

    let busPoll = poll(interval: 1, maxAttempts: 10, body: { () -> Promise<BusPollResults> in
        return self.searchService.pollBus(searchId: searchResults.info.id)
    }, signal: { (busPollResults) -> Bool in
        print("ATTEMPT MADE: PLANE POLL RESULTS: COUNT = \(busPollResults.trips.count)")
        return busPollResults.state == .finished
    })

    let planePoll = poll(interval: 1, maxAttempts: 10, body: { () -> Promise<PlanePollResults> in
        return self.searchService.pollPlane(searchId: searchResults.info.id)
    }, signal: { (planePollResults) -> Bool in
        print("ATTEMPT MADE: PLANE POLL RESULTS: COUNT = \(planePollResults.flights.count)")
        return planePollResults.state == .finished
    })

    return when(fulfilled: busPoll, planePoll) 
}

Two problems with this.

  1. It errors on the "return when" line because it says that it expects a void promise and mine is not.
  2. I don't know if its possible to do a when with different type of promises. One is for PlaneResults and the other is for BusResults.
  3. As far as my understanding goes, JOIN or WHEN, wait for all of them to complete. I don't really care, they don't need to complete "at the same time" and one does not depend on the other. I just want them to run at the same time.

So I could "separate" them out into different promise "chains", BUT they both depend on the first search promise. So in summary, they both depend on the first search promise to be able to start, but once that happens, they can go on their own separate ways and don't depend on each other in any way.

Is there an easy way to do this? Is this a dumb question :( haha

Again, thanks allot for the framework and for the help!

It errors on the "return when" line because it says that it expects a void promise and mine is not.

That's because your when returns Promise<(BusPollResults, PlanePollResults)> but you annotated the closure to return Promise<PlanePollResults>

I don't know if its possible to do a when with different type of promises. One is for PlaneResults and the other is for BusResults.

Yes, you're already doing it.

As far as my understanding goes, JOIN or WHEN, wait for all of them to complete. I don't really care, they don't need to complete "at the same time" and one does not depend on the other. I just want them to run at the same time.

Promises start immediately, so, just create them and they already run at the same time.

So I could "separate" them out into different promise "chains", BUT they both depend on the first search promise. So in summary, they both depend on the first search promise to be able to start, but once that happens, they can go on their own separate ways and don't depend on each other in any way.

Sure, just create new chains:

firstly {
    searchService.search(searchQuery)
}.then { searchResults -> Void in

    busPoll().then {
        //…    
    }

    planePoll.then {
        //…
    }
}

This is great! Can't thank you enough for this. It all makes sense now.

So promises start as soon as they're initialized, or until .then is called?

Thanks again. Using Promises is really making a really complex mess of requests of a weird API backend into a really concise easy to understand peace of code. πŸ‘

As soon as I get a good understanding of how Promises work I would love to try and contribute to this library. Thanks again!

This is the final code. Does it seem reasonable?

I was thinking, knowing that both poll promise chains have finished might be useful for UI purposes, like stopping an activity indicator for example.

I could use join or when, so the promise completes when all of them complete, that way I know when they are all finished.

BUT then I'm back to square one, because I also want to know when each one is finished, I don't need to wait for all of them to complete to update UI. That's why I followed your example of creating separate promise chains each one with their own .then handler. πŸ€” πŸ€” πŸ€”

func start() {
    firstly {
        searchService.search(searchQuery)
    }.then { searchResults -> Void in
        self.pollBus(searchId: searchResults.info.id)
        self.pollPlane(searchId: searchResults.info.id)
    }.catch { error in
        print("SEARCH ERROR = \(error)")
    }
}

func pollBus(searchId: Int) {
    poll(body: { () -> Promise<BusPollResults> in
        self.searchService.pollBus(searchId: searchId)
    }, signal: { (results) -> Bool in
        print("ATTEMPTING BUS POLL : COUNT = \(results.trips.count)")
        return results.state == .finished
    }).then { (finalResults) -> Void in
        print("FINISHED BUS POLL : COUNT = \(finalResults.trips.count)")
        dump(finalResults)
    }.catch { error in
        print("ERROR POLLING FOR BUS = \(error)")
    }
}

func pollPlane(searchId: Int) {
    poll(body: { () -> Promise<PlanePollResults> in
        self.searchService.pollPlane(searchId: searchId)
    }, signal: { (results) -> Bool in
        print("ATTEMPTING PLANE POLL")
        return results.state == .finished
    }).then { (finalResults) -> Void in
        print("FINISHED PLANE POLL")
    }.catch { error in
        print("ERROR POLLING FOR PLANE = \(error)")
    }
}

BTW your POLL implementation is working perfectly! ... Just thought of something. If one of the polls fails, like the first one out of 10 for example, I don't really care, because much more polls are coming after that one. Right now if one fails I guess the whole poll promise chain fails.

I think I need to somehow merge the POLL implementation, with the RETRY implementation in the docs, and implement some form of RECOVER. But the recover function returns a "default" value of some sort. I just really want to tell the promise chain to continue, and ignore the error. πŸ€” πŸ€” πŸ€”

So promises start as soon as they're initialized, or until .then is called?

Promises make no decision whatsoever about when the underlying tasks they represent start. See the FAQ.

This is the final code. Does it seem reasonable?

Not really. You are doing three catch handlers, one for each promise. You may as well just use completion blocks if you are going to use promises like that. One of the primary reasons to use promises is so you can consolidate error handling at a lower layer and thus handle error conditions for a wider amount of state conditions.

I was thinking, knowing that both poll promise chains have finished might be useful for UI purposes, like stopping an activity indicator for example.

I could use join or when, so the promise completes when all of them complete, that way I know when they are all finished.

BUT then I'm back to square one, because I also want to know when each one is finished, I don't need to wait for all of them to complete to update UI. That's why I followed your example of creating separate promise chains each one with their own .then handler.

Don’t forget, promises are composable:

firstly {
    searchService.search(searchQuery)
}.then { searchResults -> Void in

    let p1 = busPoll().then {
        //…    
    }

    let p2 = planePoll.then {
        //…
    }

    return when(fulfilled: p1, p2)
}

I think I need to somehow merge the POLL implementation, with the RETRY implementation in the docs, and implement some form of RECOVER. But the recover function returns a "default" value of some sort. I just really want to tell the promise chain to continue, and ignore the error.

The only logical way recover could work is if it returns a default value, for example:

foo().then {
    return 1
}.recover {
    // ignore error
}.then { x in
   // what is x if recover returns nothing? This is an impossible situation.
}

Swift itself cannot compile this scenario, which is a reason promises in typed languages work so well.

If you find yourself with no logical value to return then you should probably be using Void promises.

This is awesome! Can't thank you enough.

Ok. I will consolidate all error handling to a single .catch block. The only issue I thought that could happen was that I would get a generic API error and I would not know to which of the requests that error belongs to.

BUT I guess I could just create a more specific error type for each of the requests which wraps the api error as an associated value. Makes sense?

Will post the final "correct" code in a second just in case some other Promise newbie arrives here πŸ‘

Just say your comment on recover. Totally get it.

I guess what I was trying to do is something more like this.

func poll<T>(interval: TimeInterval = 1.0, maxAttempts: Int = 20, body: @escaping () -> Promise<T>, signal: @escaping (T) -> Bool) -> Promise<T> {
    var attempts = 0
    func attempt() -> Promise<T> {
        attempts += 1
        return body().then { result -> Promise<T> in
            guard attempts < maxAttempts else { throw PollingError.maxAttemptsReached(forType: T.self, maxAttempts: maxAttempts) }
            return signal(result) ? Promise(value: result) : after(interval: interval).then(execute: attempt)
        }.recover { error -> Promise<T> in
            guard attempts < maxAttempts else { throw PollingError.maxAttemptsReached(forType: T.self, maxAttempts: maxAttempts) }
            return after(interval: interval).then(execute: attempt)
        }
    }
    return attempt()
}

I basically "merged" my own polling with the retry sample code. This way, if somewhere in the middle of the 20 attempts there is an error, it "ignores" it and continues on the chain.

Just realized this is possibly going to create two separate promise chains. damn. :(

Just realized this is possibly going to create two separate promise chains. damn. :(

It does not, at least the code above does not.

Oh awesome! ... I guess not.

So do you think the above POLL function would achieve the objective of polling 20 times, and ignoring any error that comes along, UNTIL it reaches the max attempts and then errors out.

Cool πŸ‘

Yes it should do that.

Might be sensible not to ignore all errors though.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

katopz picture katopz  Β·  4Comments

mxcl picture mxcl  Β·  4Comments

emreond picture emreond  Β·  5Comments

Banck picture Banck  Β·  4Comments

daanporon picture daanporon  Β·  6Comments