Skip to content

OutOfMemory due to results in chain retained #1529

Description

@peterlundberg
  1. What version of bluebird is the issue happening on?
    3.5.1

  2. What platform and version? (For example Node.js 0.12 or Google Chrome 32)
    Node v8.11.1 on osx Darwin Kernel Version 17.5.0 x86_64

  3. Did this issue happen with earlier version of bluebird?
    Not sure


Using promise chains can cause unexpected OutOfMemory situations as the partial results in the chain seam to be retained until the entire chain is settled.

For example in this promise-chain-retain.js:

// global.Promise = require('bluebird');
console.log('Using', Promise.version || 'vanilla Promises');
logMemoryUse('before all');

Promise.resolve()
.then(() => {
  logMemoryUse('build result');
  const rows = [];
  for (let i = 0; i < 1000000; i += 1) {
    rows.push([`Example ${i}`, i, i * 2]);
  }
  return Promise.resolve({ rows });
})
.then((result) => {
  logMemoryUse('use result');
  // this then gets the large result
  return { len: result.rows.length };
})
.then((summaryResult) => {
  logMemoryUse('use summary');
  // this then should not need the 'result' and be free to reuse heap
  console.log(summaryResult);
})
.catch((err) => {
  console.log('caught error: ', err);
});

setTimeout(() => logMemoryUse('after all'), 3000);

function logMemoryUse(prefix = '') {
  global.gc();
  global.gc();
  const toMiB = b => (b / 1048576).toFixed(2);
  const mem = process.memoryUsage();
  console.log([
    'memoryUsage:',
    prefix.padEnd(20),
    `rss ${toMiB(mem.rss)} Mb`.padStart(20),
    // `heapTotal ${toMiB(mem.heapTotal)} Mb`.padStart(20),
    `heapUsed ${toMiB(mem.heapUsed)} Mb`.padStart(20),
  ].join('  '));
}

Using node 8 vanilla promises this works as expected in a chain and in "use summary" step the big result is no longer retained and can be GC:ed:

❯ NODE_ENV="production" node --expose-gc promise-chain-retain.js
Using vanilla Promises
memoryUsage:  before all                    rss 21.21 Mb    heapUsed 3.95 Mb
memoryUsage:  build result                  rss 21.28 Mb    heapUsed 3.94 Mb
memoryUsage:  use result                   rss 192.95 Mb    heapUsed 151.17 Mb
memoryUsage:  use summary                  rss 134.85 Mb    heapUsed 4.20 Mb <== GC worked!
{ len: 1000000 }
memoryUsage:  after all                     rss 44.03 Mb    heapUsed 4.22 Mb

When using Bluebird as Promise things do not work as expected. The result of the first then is still retained in "use summary" step.

❯ NODE_ENV="production" node --expose-gc promise-chain-retain.js
Using 3.5.1
memoryUsage:  before all                    rss 25.03 Mb    heapUsed 4.77 Mb
memoryUsage:  build result                  rss 25.12 Mb    heapUsed 4.75 Mb
memoryUsage:  use result                   rss 194.18 Mb    heapUsed 152.00 Mb
memoryUsage:  use summary                  rss 195.88 Mb    heapUsed 152.00 Mb  <== WHY?
{ len: 1000000 }
memoryUsage:  after all                    rss 113.99 Mb    heapUsed 5.05 Mb

Thus long chains in a request do not GC as expected if resolving large values. This example is simplified but imagine building a large object, converting to JSON, compressing and then sending to an external service - if at any step resolving the large object will not allow it to be GC:ed until the entire chain is done. Essentially at least doubling the memory requirement (or OOM given contained resources and large objects).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions