2

Let's say I want to run a lot of promises (which are not yet declared, so their body has not executed yet). I don't want to run them in sequence because that would be very slow but I don't want all of them to run in parallel because that would overload the server. Each of this promises can be resolve or rejected very fast (around 1 second) or, in the worst case scenario, after something around 40 seconds. None of the promises depend on the result of any other. Which is the best way to handle this case? I have seen that an alternative is using Promise.all() in batches of 10 promises. However, the latter doesn't seem to be efficient considering that I can have 9 promises resolving very fast, but 1 of them resolving very slow. What I would like the following:

Say I have 100 promises. I start processing 10 of them, as soon as one of them resolves, the 11th promise is processed, as soon as other one resolves, the 12th promise is processed, and so on. Notice this behavior is different from using Promise.all() in batches of 10 because in that case the 11th to 20th promise would be processed after all of the 1st to 10th promises are resolved.

Which is the best way of implementing this? I was picturing something like having a queue and having an async function processPromise that pops promises from that queue (until it is empty) and process them. I would then make an array of promises where each promise corresponds to an invocation of processPromise and then Promise.all() over that array. However, I'm not sure if concurrent access to that queue (that is share by different invocations of processPromise) would be an issue, or if I have nothing to worry about considering javascript is single-threaded.

Juan Chaves
  • 81
  • 1
  • 4
  • Why do you need to execute the promises in batches? – Cully Mar 14 '22 at 03:49
  • Would this work? https://www.npmjs.com/package/p-limit – Cully Mar 14 '22 at 03:51
  • 1
    Promise.all will reject immediately if any input promise rejecting so it might not be suitable for this case. Have you tried Promise.allSettled? – Huy Duy Mar 14 '22 at 03:52
  • Use something like `mapConcurrent()` [here](https://stackoverflow.com/questions/46654265/promise-all-consumes-all-my-ram/46654592#46654592) that lets you send N requests in parallel and each time one finishes, it starts the next one. Note, this works on an array of data, not an array of promises because once you have an array of promises, all the operations are already running. There are a couple other options `pMap()` and `rateLimitMap()` [here](https://stackoverflow.com/questions/66991018/how-to-download-huge-list-of-remote-files-using-node-js-http-get-method-without/66991338#66991338) too. – jfriend00 Mar 14 '22 at 04:04
  • 2
    "*I have an array with promises*" - if you got that far, you already have started all of the tasks for whose results you got the promises for, and they are already running - presumably in parallel - without any way to affect that. – Bergi Mar 14 '22 at 04:07
  • @Cully I would not like to overload the server. I'm working with an api and I wouldn't like to exceed the established quotas per second. – Juan Chaves Mar 14 '22 at 04:13
  • 1
    @Bergi yeah, I might have use a bad wording, what I have is an array of data that I'm going to use to build the promises. I'm aware that the tasks start when the promises are constructed. Thanks for the comment, I'll edit the post to make that clear :) – Juan Chaves Mar 14 '22 at 04:17
  • @jfriend00 looks that `mapConcurrent()` is what I'm looking for. Looks to me like a similar implementation of the package @Cully suggested so thank you both for your answers. It looks to me that the implementation of `mapConcurrent` rejects the big Promise as soon as one of the mapped promises rejects, but a slight modification will do for me. Thanks :) – Juan Chaves Mar 14 '22 at 04:41
  • @JuanChaves - If you don't want it to stop on a rejection, then you can just use a `.catch()` on the promise that your function gets so that it never gives a rejected promise back to `mapConcurrent()` and that can be done without changing `mapConccurrent()`. – jfriend00 Mar 14 '22 at 04:44

1 Answers1

0

I have used promise-pool for this:

        const { results } = await PromisePool.withConcurrency(10)
            .for(tasks)
            .process(async task => {
                const result = await this.longTask(task);
                return { result };
            });

Note that surprisingly results are not always returned in the order you provided them, so depending on your use case you may have to track the original index and sort upon completion:

        const tasks = items.map((item, index) => ({ item, index }));
        const { results } = await PromisePool.withConcurrency(10)
            .for(tasks)
            .process(async task => {
                const result = await this.longTask(task.item);
                return { result, index: task.index };
            });
        const ordered = results
            .sort((a, b) => a.index - b.index)
            .map(result => result.result);
Myk Willis
  • 12,306
  • 4
  • 45
  • 62