10

Is there a way/pattern to implement let res = Promise.all([...p], limit)?

  • The promise-array holds functions which construct and return the promise
  • res should be resolved after all p resolves
  • Only limit=3 Promises should run in parallel
  • The n+1th Promise should start immediately after n finishes. So that there are always limit resolvers running in parallel.

Especially the last point creates my headaches.

My current solution is to split the promises-array into chunks of limit size and chain them. The disadvantages here is that the second bunch don't start until all Promises from bunch 1 has been resolved.

Psi
  • 474
  • 4
  • 14
  • Promises generally begin running the moment you create them. `Promise.all`, or anything else you pass them into once they've already been made, can't control whether they run or not. If you want to control their parallelism, that needs to take place at an earlier stage in the game. – JLRishe Nov 02 '16 at 08:48
  • Yes of course. The promise-array holds functions which construct and return promises. I expressed that not clear enought and I edited my question. – Psi Nov 02 '16 at 09:01
  • 1
    Isn't this a duplicate of: https://stackoverflow.com/questions/40639432/what-is-the-best-way-to-limit-concurrency-when-using-es6s-promise-all – Venryx Oct 16 '19 at 01:01
  • 2
    Possible duplicate of [What is the best way to limit concurrency when using ES6's Promise.all()?](https://stackoverflow.com/questions/40639432/what-is-the-best-way-to-limit-concurrency-when-using-es6s-promise-all) – Venryx Oct 16 '19 at 01:02

3 Answers3

8

I came up with the idea of creating n = limit chains which run in parallel and append as long as there are promises:

let promises = [];
for(let i=0; i<11; i++) promises[i] = () => {
  console.log('Construct:',i);
  return new Promise(resolve => {
    setTimeout(function() {
      console.log('Resolve:',i);
      resolve(i);
    }, Math.round(Math.random() * (2000 - 500) + 2000));
  });
}


function parallelLimit(promiseFactories, limit) {
  let result = [];
  let cnt = 0;

  function chain(promiseFactories) {
    if(!promiseFactories.length) return;
    let i = cnt++; // preserve order in result
    return promiseFactories.shift()().then((res) => {
      result[i] = res; // save result
      return chain(promiseFactories); // append next promise
    });
  }

  let arrChains = [];
  while(limit-- > 0 && promiseFactories.length > 0) {
    // create `limit` chains which run in parallel
    arrChains.push(chain(promiseFactories));
  }

  // return when all arrChains are finished
  return Promise.all(arrChains).then(() => result);
}


parallelLimit(promises, 4).then(console.log);

Excited to read your comments and suggestions :)

Psi
  • 474
  • 4
  • 14
  • 1
    Note: this does what it is suppose to do - it runs, as in example, 4 promises at a time. if it completes 1 / 4, it adds new one to the parallel pool. I just thought, it would finish first 4, then do next 4. Really nice snippet. Just to note - you have named your array `promises`. But it actually doesnt contain promises. It contains promise factories (functions, that return promises). – Wish Sep 26 '17 at 11:50
  • This is a really nice answer. Consider making these 2 enhancements if you incorporate this into production code. 1: handle errors in the chain function. The second arg to `then` is handy for this. 2: transform the result array before returning so it looks like the user called `Promise.allSettled` on a huge batch of requests. This gives the caller a chance to deal with failures. – bigh_29 Nov 24 '21 at 06:12
1

The sequenceWithParallelism function in the bluebird-as extension appears to be designed for precisely the functionality you want. As written, it uses bluebird for its implementation, but I don't see anything bluebird-specific in the actual contents of the sequenceWithParallelism function, so you could just excerpt that function and use it in your own code, like:

sequenceWithParallelism(limit, f => f())([..p])
    .then(function () {

    });

Oddly, the function doesn't seem to be designed to resolve to the results of all of the promises, so if you need that, you'd probably need to make a few adjustments.

JLRishe
  • 99,490
  • 19
  • 131
  • 169
0

I ended up coding the following:

const parallelLimit = (asyncFuncs, limit) => {
  return new Promise((res, rej) => {
    let open = 0
    const rets = [], errs = []
    const exec = () => { while(ff.length && open < limit) ff.shift()() }
    const ff = asyncFuncs.map(f => {
      return () => {
        open++
        f()
          .then(r => rets.push(r))
          .catch(e => errs.push(e))
          .finally(_ => {
            open--
            exec()
            if(!ff.length && !open)
              return errs.length ? rej(errs) : res(rets)
        })
      }
    })
    exec()
  })
}

// test:

const t = Date.now()
const funcs = [...Array(10).keys()].map(idx => {
  return () => {
    return new Promise((res, rej) => {
      setTimeout(() => {
        console.log(`idx: ${idx} time: ${Date.now() - t}`)
        res(idx)
      }, 1000)
    })
  }
})

parallelLimit(funcs, 2)
  .then(res => console.log(`then ${res.join(',')}`))
  .catch(errs => console.log(`catch ${errs.join(',')}`))
youurayy
  • 1,635
  • 1
  • 18
  • 11