Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Bluebird "map" return early?

Let's imagine that I have 10 promises running that will be passed to Promise.map() with its default concurrency setting of 3.

If any of the first 3 promises are rejected, will the other 7 be even started?

Bluebird's docs for map() states the following:

Promises returned by the mapper function are awaited for and the returned promise doesn't fulfill until all mapped promises have fulfilled as well. If any promise in the array is rejected, or any promise returned by the mapper function is rejected, the returned promise is rejected as well.

It only states that if all fulfill, then the inner promises are awaited, but it's not clear to me what happens if any of them is rejected.

Edit

I'm aware that the canonical definition of the map function is that the output's length is the same as the size of the input, but I'm not sure if Bluebird honors this.

like image 537
Henrique Barcelos Avatar asked Nov 15 '16 20:11

Henrique Barcelos


2 Answers

I have achieved very similar results to @ssube's answer.

I got 10 promises, that will be resolved or rejected after a increasing timeout. The 4th one (because the array is 0-based) is rejected.

const Promise = require('bluebird')

function delay(timeout, err, i) {
  return new Promise(function (resolve, reject) {
    if (err) {
      setTimeout(function () {
        console.log('Rejected', err.message)
        reject(err)
      }, timeout)
    } else {
      setTimeout(function () {
        console.log('Resolved', i)
        resolve(i)
      }, timeout)
    }
  })
}

const promises = Array.apply(null, {length: 10})
  .map(Function.call, Number)
  .map(function (it) {
    if (it === 3) {
      return delay(500 * it, new Error(it))
    } else {
      return delay(500 * it, null, it)
    }
  })

Promise.map(promises, function (p) {
  console.log('Mapping', p)
  return p.toString()
})
  .then(function (it) {
    console.log('All resolved', it)
  })
  .catch(function (err) {
    console.log('Error', err.message)
  })

This will yield:

> Resolved 0
> Mapping 0
> Resolved 1
> Mapping 1
> Resolved 2
> Mapping 2
> Rejected 3
> Error 3
> Resolved 4
> Resolved 5
> Resolved 6
> Resolved 7
> Resolved 8
> Resolved 9

So, the behaviour is the following:

  • Promise.map is short-circuited whenever one of the promises being mapped is rejected.
  • The callback in map is never executed for any of the subsequent promises.
  • Notice that Error 3 comes before the deliberately slower promises.
  • However, all of the promises are "executed" until they are settled.
like image 20
Henrique Barcelos Avatar answered Oct 20 '22 00:10

Henrique Barcelos


Taken literally, Bluebird's Promise.prototype.map returns immediately.

When doing so, it returns a Promise that may not resolve immediately. I imagine you really want to know how that promise behaves and there are a few things to break down here:

If any of the first 3 promises are rejected, will the other 7 be even started?

Yes. Promises are "started" (that is, scheduled) when you create them. The other 7 will attempt to resolve or are likely enough to do so that you need to assume they will.

Imagine if the browser only allows 4 HTTP connections to a server and you make 10 requests. Those first (failing) 3 will be sent, along with a friend, who may not fail but will certainly run.

You should assume that all promises will invoke their bodies.

It only states that if all fulfill, then the inner promises are awaited, but it's not clear to me what happens if any of them is rejected.

That's easy enough to test:

const Promise = require('bluebird');                    

function delayReject(delay, err) {                      
  return new Promise((res, rej) => {                    
    console.log('waiting to reject', err);              
    setTimeout(() => rej(err), delay);                  
  });                                                   
}                                                       

function delayValue(delay, val) {                       
  return new Promise((res, rej) => {                    
    console.log('waiting to resolve', val);             
    setTimeout(() => res(val), delay);                  
  });                                                   
}                                                       

const promises = [1, 2, 3, 4, 5, 6, 7, 8, 9].map(it => {
  if (it % 3 === 0) {                                   
    return delayReject(50, it);                         
  } else {                                              
    return delayValue(50, it);                          
  }                                                     
});                                                     

Promise.map(promises, v => {                            
  console.log('mapping', v);                            
  return -v;                                            
}).then(it => {                                         
  console.log('mapped', it);                            
}).catch(err => {                                       
  console.log('error', err);    
});                        

My output, with node v6.8.1, is:

ssube@localhost ~/questions/40619451 $  > node early-exit.js
waiting to resolve 1                                                      
waiting to resolve 2                                                      
waiting to reject 3                                                       
waiting to resolve 4                                                      
waiting to resolve 5                                                      
waiting to reject 6                                                       
waiting to resolve 7                                                      
waiting to resolve 8                                                      
waiting to reject 9                                                       
mapping 1                                                                 
mapping 2                                                                 
error 3 

As you may expect, all the promises are scheduled and run, but the map does stop running against them after the first failure.

The Bluebird docs mention that:

The mapper function for a given item is called as soon as possible, that is, when the promise for that item's index in the input array is fulfilled. This doesn't mean that the result array has items in random order, it means that .map can be used for concurrency coordination unlike .all.

That suggests that the order of mapped items may not be persisted, like it is in the above example. We can test that by adding some noise to the delay:

const Promise = require('bluebird');                    

function delayNoise(n) {                                
  return n + Math.floor(Math.random() * 50);            
}                                                       

function delayReject(delay, err) {                      
  return new Promise((res, rej) => {                    
    console.log('waiting to reject', err);              
    setTimeout(() => rej(err), delayNoise(delay));      
  });                                                   
}                                                       

function delayValue(delay, val) {                       
  return new Promise((res, rej) => {                    
    console.log('waiting to resolve', val);             
    setTimeout(() => res(val), delayNoise(delay));      
  });                                                   
}                                                       

const promises = [1, 2, 3, 4, 5, 6, 7, 8, 9].map(it => {
  if (it % 3 === 0) {                                   
    return delayReject(50, it);                         
  } else {                                              
    return delayValue(50, it);                          
  }                                                     
});                                                     

Promise.map(promises, v => {                            
  console.log('mapping', v);                            
  return -v;                                            
}).then(it => {                                         
  console.log('mapped', it);                            
}).catch(err => {                                       
  console.log('error', err);                            
});                                  

Running that yields far more interesting results: if the first promise rejects, then the map ends and does not attempt to map the others. It does short-circuit, as you guessed.

like image 51
ssube Avatar answered Oct 19 '22 22:10

ssube