Setting a timeout for each promise within a promise.all
Asked Answered
D

3

15

I am able to successfully perform a Promise.all, and gracefully handle resolves and rejects. However, some promises complete within a few milliseconds, some can/could take a while.

I want to be able to set a timeout for each promise within the Promise.all, so it can attempt to take a maximum of say 5seconds.

getData() {
    var that = this;
    var tableUrls = ['http://table-one.com','http://table-two.com'];
    var spoonUrls = ['http://spoon-one.com','http://spoon-two.com'];

    var tablePromises = that.createPromise(tableUrls);
    var spoonPromises = that.createPromise(spoonUrls);
    var responses = {};

    var getTableData = () => {
        var promise = new Promise((resolve, reject) => {
            Promise.all(tablePromises.map(that.rejectResolveHandle))
                .then((results) => {
                    responses.tables = results.filter(x => x.status === 'resolved');
                    resolve(responses);
                });
        });
        return promise;
    };

    var getSpoonData = () => {
        var promise = new Promise((resolve, reject) => {
            Promise.all(spoonPromises.map(that.rejectResolveHandle))
                .then((results) => {
                    responses.tables = results.filter(x => x.status === 'resolved');
                    resolve(responses);
                });
        });
        return promise;
    };


    return getTableData()
        .then(getSpoonData);
}

rejectResolveHandle() {
    return promise.then(function(v) {
        return {v:v, status: "resolved"};
    }, function(e) {
        return {e:e, status: "rejected"};
    });
}

createPromise(links) {
    var promises = [];
    angular.forEach(links, function (link) {
        var promise = that._$http({
            method: 'GET',
            url: link + '/my/end/point',
            responseType: 'json'
        });
        promises.push(promise);
    });

    return promises;
}

I have tried adding a timeout to createPromise(), however this does not seem to work. Setting a timeout to 300ms, some requests continue for 4+seconds:

createPromise(links) {
    var promises = [];
    angular.forEach(links, function (link) {
        var promise = that._$http({
            method: 'GET',
            url: link + '/my/end/point',
            responseType: 'json'
        });

        promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(promise);
            }, 300);
        });

        promises.push(promise);
    });

    return promises;
}

I have access to Bluebird if it will makes things easier?

Droplight answered 2/2, 2018 at 7:26 Comment(6)
Is using RxJs / Observables an possible option ?Grundy
@Grundy - RxJs yes, version 5.4.0... and observe-js version 0.5.7Droplight
Do you really want to put a timeout on each and every promise, or do you really just want one timer that will cause the whole thing to fail, if anything takes too long? Remember that the whole Promise.all fails, if any one fails. With that in mind, is there reason, then, not to simply add a wrapper that fails in ...say... 3 seconds, if the whole thing isn’t done?Mammalogy
@Norguard - it should be for each promise, rather than all. Understand the whole Promise.all can fail, and hence why they are handled in rejectResolveHandleDroplight
When asking a question about a problem caused by your code, you will get much better answers if you provide code people can use to reproduce the problem. That code should be… Minimal – Use as little code as possible that still produces the same problem. See How to create a Minimal, Complete, and Verifiable example.Stenopetalous
Both the getTableData and getSpoonData functions are defered anti-patterns. Also Promise.all is not integrated with the AngularJS framework. Only operations which are applied in the AngularJS execution context will benefit from AngularJS data-binding, exception handling, property watching, etc. Use $q.all.Stenopetalous
A
33

Here's a scheme that creates a Promise.raceAll() function that works kind of like a combination of Promise.all() and Promise.race() where the promises all have a timeout time and value so that if the promise doesn't resolve before that time, it will be short circuited to resolve with the passed in value. This essentially puts each promise into a Promise.race() with a timer. If the timer wins, the promise is resolved with the default value. If the original promise wins, it's resolved with the actual promise result. We use Promise.race() to resolve with the first one to finish (the timeout or the original promise). This is a classic use for Promise.race() (in fact the only practical use I've ever really used it for).

A classic example would be to get me all the results you can in the next 15 seconds. Any results that take longer than 15 seconds, just return null for them and don't wait for them. Here's the code to make this concept work:

Promise.delay = function(t, val) {
    return new Promise(resolve => {
        setTimeout(resolve.bind(null, val), t);
    });
}

Promise.raceAll = function(promises, timeoutTime, timeoutVal) {
    return Promise.all(promises.map(p => {
        return Promise.race([p, Promise.delay(timeoutTime, timeoutVal)])
    }));
}

So, you use Promise.raceAll() like Promise.all() in that you pass it an array of promises, but you also pass it a timeoutTime and a timeoutVal. The timeoutTime is the how long to wait before timing out the promises. The timeoutVal is what to put in the results array for any promise that timed out (often it will be something like null that you can easily recognize as a non-real result).


I'm not sure I entirely what you are doing in your specific code, but here's your links code using the above:

Promise.raceAll(links.map(link => {
    return that._$http({
        method: 'GET',
        url: link + '/my/end/point',
        responseType: 'json'
    });
}), 5000, null).then(results => {
    // process results here
    // any timed out values will be null
    // you can filter out the timed out results
    let final = results.filter(item => !!item);
}).catch(err => {
    // process any errors here
});

Or, if you want to make sure Promise.raceAll() gets all results, even if some promises reject, you can add a .catch() handler to each promise:

Promise.raceAll(links.map(link => {
    return that._$http({
        method: 'GET',
        url: link + '/my/end/point',
        responseType: 'json'
    }).catch(err => {
        // don't let Promise.all() see a reject so it gets all results
        return null;
    });
}), 5000, null).then(results => {
    // process results here
    // any timed out values will be null
    // you can filter out the timed out or rejected results
    let final = results.filter(item => !!item);
}).catch(err => {
    // process any errors here
});
Abacist answered 2/2, 2018 at 8:15 Comment(11)
i like this idea, but integrated with mine would be usefulDroplight
@OamPsy - I had trouble understanding exactly how your question applied to your specific code. I added an example using your array of links.Abacist
I should have thought of Promise.race! Esp. since one frequently has a delay-like function lying around giving us a promise version of setTimeout...Petasus
Thanks! Do i stll need the getTableData() and getSpoonData methods? Im trying to see how the whole thing fits together...Droplight
@OamPsy - I honestly have no idea what all that code is about. It doesn't appear to have anything to do with the question you asked. I answered exactly what you asked for in the title and first two paragraphs of your question and showed you an example using your last code block. I don't know what getTableData() or getSpoonData() have to do with your question and you did not explain what they are supposed to do or ask any questions about them. If you have questions about them, perhaps you should write a new question and ask a specific question about that code.Abacist
@OamPsy - If your confusion in getTableData() and getSpoonData() is about how to best handle rejected promises so you still get all your results from Promise.all(), then you can see Promise.settleVal() in this answer ES6 Promise.all() error handle - Is .settle() needed? and it might work for you.Abacist
@OamPsy - I add a second code example so that Promise.raceAll() gets all the results, even if some promises reject.Abacist
@OamPsy - Did this answer your question?Abacist
@Abacist I dont believe so. Some requests (when inspecting dev tools Networks) will go on (Pending) for over 10seconds, and the page freezes until these resolve.Droplight
@OamPsy - Well, the concept of Promise.raceAll() works here: jsfiddle.net/jfriend00/ktgk5ue6. That starts 10 ajax calls that are configured to take 1-10 seconds to finish. Then, when I use Promise.raceAll() with them and pass 3500ms as the timeout, Promise.raceAll() calls it's .then() handler after 3500ms and has the expected 3 results. The other ajax calls finish some time later and their result is ignored.Abacist
@OamPsy - If you want help with your specific implementation, you'd have to add your actual code using Promise.raceAll() as an edit to the end of your question so we can see what you did with it.Abacist
P
4

Your attempt just overwrites promise with the timeout promise, meaning that the HTTP promise will be completely ignored.

This is one of the relatively few places where you'd actually use new Promise*: Create a promise, resolve or reject it based on the HTTP call, and reject it (or resolve it, but that seems odd) on timeout:

createPromise(links) {
    return Promise.all(links.map(function(link) {
        return new Promise((resolve, reject) => {
            that._$http({
                method: 'GET',
                url: link + '/my/end/point',
                responseType: 'json'
            })
            .then(resolve)
            .catch(reject);
            setTimeout(() => {
                reject(/*...relevant value here...*/); // Seems like reject to me, 
                                                       // but you could use resolve
                                                       // if you prefer
            }, 300);
        });
    }));
}

(That uses map on the assumption that links is an array.)

Note that once resolve or reject has been called for a promise, subsequent calls to either are completely ignored (without error or warning). (If that weren't true, we'd want a flag above to track whether we'd settled the promise already.)


* (Unless you have a promise-enabled version of setTimeout. If you do, you'd use Promise.race like jfriend00 did in their answer.)

Petasus answered 2/2, 2018 at 7:31 Comment(10)
Thanks! i can see an error in the console: Uncaught (in promise) {data: null, status: -1, headers: f, config: {}, statusText: ""}Droplight
@OamPsy: That won't be related to the above, unless you're not handling rejections of the promise createPromise returns. Or unless links is not an array (I assumed it was and used map).Petasus
Links, is an array too.Droplight
"unless you're not handling rejections of the promise createPromise returns" - should the rejected be filtered out?Droplight
@OamPsy: I'm afraid I don't know what you mean by "filtered out." The createPromise above returns a promise that will resolve when all the HTTP calls have completed, or reject if A) Any of them fails, or B) Any of them times out. If you change the reject(...) to resolve(...) on the line I indicated with a comment, it will return a promise that will resolve when all of the HTTP calls have either completed or timed out, or reject if any of them fails before its timeout. Nothing in there "filters out" rejections. (cont'd)Petasus
(continuing) You have to handle the fact the promise may reject (fail). (cont'd) You always do, it's one of the rules of promises: You either handle errors, or you pass the promise on to your caller (who will handle errors, or pass it on, etc.). The top level must always handle errors.Petasus
Understood, but what im asking is how do i handle it. Ive used your example above, and i can see the the return of promises, some are fulfiled, and some are rejected. How can i filter out the rejected e.g response: Promise [[PromiseStatus]]: "rejected'.. How do i access PromiseStatus. I have tried promises[0].PromiseStatusDroplight
@OamPsy: Sorry, I'd accidentally left out Promise.all in the code! My description above is only valid if the map is wrapped in Promise.all (which it was meant to be, and is now, hit refresh). You don't need to access the individual status of the promises. The one promise createPromise returns either resolves or rejects.Petasus
Thanks, a tad better. Now seeing: tablePromises.map is not a functionDroplight
@OamPsy: Obviously, tablePromises is the result of calling createPromise above, which is a promise, not an array. If you want an array, don't put Promise.all in createPromises. All the tools you need are described, in detail, above. It's up to you to apply them.Petasus
B
0

One solution might be to convert each promise in an argument array for Promise.all into a timed out promise (say using .map). The timeout function could resolve or reject a supplied argument promise depending on application needs.

Using a unique value to resolve a promise after timeout allows it to be identified in the result array supplied to a Promise.all.then( handler)

This example looks out converting a promise into a resolved promise using a helper function timedPromise. Mapping a promise array to an array of timed out promises is not included.

const timedOutValue = new Error( "promise timeout"); // unique value
const timedPromise = (promise, timeout) => {
    return new Promise( (resolve, reject) => {
      promise.then(resolve, reject);
      setTimeout( resolve, timeout, timedOutValue);  // could have used reject
    });
};

var pendingPromise = new Promise( (resolve,reject) => null);  // never settles 
timedPromise(pendingPromise)
.then(
    data=> console.log( "pendingPromise resolved with %s",
       data === timedOutValue ? "timedOutValue" : data),
    reason=> console.log( "pendingPromise rejected with %s",
       reason === timedOutValue ? "timedOutValue" : reason)
);
 
Banwell answered 2/2, 2018 at 8:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.