Jeff-Lewis / cls-hooked

cls-hooked : CLS using AsynWrap or async_hooks instead of async-listener for node 4.7+
BSD 2-Clause "Simplified" License
759 stars 89 forks source link

[Memory leak] Context is not cleared if it references a Promise #66

Open MayurRJoshi opened 3 years ago

MayurRJoshi commented 3 years ago

Summary If the context contains a key which references a Promise, then that context object is never cleared. Hence, the _contexts map keeps growing and hogs up all the heap memory.

Steps to reproduce

  1. Install express and cls-hooked.
  2. Run the script below, node --inspect index.js
    
    const express = require('express')
    const clsHooked = require('cls-hooked')
    clsHooked.createNamespace('test')

const app = express()

app.get('/', (req, res, next) => { const namespace = clsHooked.getNamespace('test')

console.log(namespace._contexts)

namespace.run(() => {
    namespace.set('x', Promise.resolve())
    res.json({})
})

})

app.listen(3030, () => console.log(('Listening')))

3. Call the server api, `curl localhost:3030`
4. [Optional] Manually trigger a garbage collection from the Chrome debugger.
5. Repeat 3 and 4 and observe the logs. Notice that the `_contexts` keeps growing even after a manual garbage collection

node --inspect index.js Debugger listening on ws://127.0.0.1:9229/05303a92-c1ac-4cdb-816a-4b5e15de611a For help, see: https://nodejs.org/en/docs/inspector Listening Debugger attached. Map {} Map { 16 => { _ns_name: 'test', id: 11, x: Promise { undefined } }, 22 => { _ns_name: 'test', id: 11, x: Promise { undefined } } } Map { 16 => { _ns_name: 'test', id: 11, x: Promise { undefined } }, 39 => { _ns_name: 'test', id: 34, x: Promise { undefined } } } Map { 16 => { _ns_name: 'test', id: 11, x: Promise { undefined } }, 39 => { _ns_name: 'test', id: 34, x: Promise { undefined } }, 61 => { _ns_name: 'test', id: 56, x: Promise { undefined } } } Map { 16 => { _ns_name: 'test', id: 11, x: Promise { undefined } }, 39 => { _ns_name: 'test', id: 34, x: Promise { undefined } }, 61 => { _ns_name: 'test', id: 56, x: Promise { undefined } }, 84 => { _ns_name: 'test', id: 79, x: Promise { undefined } }, 90 => { _ns_name: 'test', id: 79, x: Promise { undefined } } }


If I change the `set` to `namespace.set('x', '')` (or any other primitive or object), context is cleared sometimes automatically after every request and **always** if a manual garbage collection is triggered.

Output with no manual GC.

node --inspect index.js Debugger listening on ws://127.0.0.1:9229/d39a47e8-72a2-4603-885a-ba0e3dbd8d53 For help, see: https://nodejs.org/en/docs/inspector Listening Map {} Map { 21 => { _ns_name: 'test', id: 11, x: '' } } Map {} Map {} Map {}