-
Notifications
You must be signed in to change notification settings - Fork 29.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Memory leak when a Promise is stored on an Express request that's added to a domain #23862
Comments
Ouch. I think the cause here is a circular-ish reference chain…
A regular circular reference would be no problem for GC, but the fact that we have this global |
@addaleax lib/domain.js:51:const pairing = new Map();
lib/domain.js:56: pairing.set(asyncId, process.domain);
lib/domain.js:61: const current = pairing.get(asyncId);
lib/domain.js:67: const current = pairing.get(asyncId);
lib/domain.js:73: pairing.delete(asyncId); // cleaning up There seems to be no iteration. |
@ChALkeR Yes, that’s the map – but it’s… difficult. I don’t have a good idea of how this could be addressed at the moment. |
This is a fundemental design flaw of async_hooks. We use numbers as indexes (so no weakmaps) on a global map to allocate/track state and we deallocate it only when objects are destroyed. In several cases, this are tied to when the garbage collector actually cleans up the object, making it unpredictable. In the past, I proposed to introduce a currentAsyncResource() API to be able to store state on the resource itself, removing the need for the global map. I’ll resume that work asap. |
So… one thing we could do is to turn the values in the map into weak references… that’s icky, but it solves this particular problem. It would mean that we rely on the Example diff for v10.x in the folddiff --git a/lib/domain.js b/lib/domain.js
index e8ae3ff10034..b66fdc3f3a09 100644
--- a/lib/domain.js
+++ b/lib/domain.js
@@ -35,6 +35,9 @@ const {
} = require('internal/errors').codes;
const { createHook } = require('async_hooks');
+const kWeak = Symbol('kWeak');
+const { WeakReference } = internalBinding('util');
+
// overwrite process.domain with a getter/setter that will allow for more
// effective optimizations
var _domain = [null];
@@ -53,7 +56,7 @@ const asyncHook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
if (process.domain !== null && process.domain !== undefined) {
// if this operation is created while in a domain, let's mark it
- pairing.set(asyncId, process.domain);
+ pairing.set(asyncId, process.domain[kWeak]);
resource.domain = process.domain;
if (resource.promise !== undefined &&
resource.promise instanceof Promise) {
@@ -67,13 +70,13 @@ const asyncHook = createHook({
before(asyncId) {
const current = pairing.get(asyncId);
if (current !== undefined) { // enter domain for this cb
- current.enter();
+ current.get().enter();
}
},
after(asyncId) {
const current = pairing.get(asyncId);
if (current !== undefined) { // exit domain for this cb
- current.exit();
+ current.get().exit();
}
},
destroy(asyncId) {
@@ -174,6 +177,7 @@ class Domain extends EventEmitter {
super();
this.members = [];
+ this[kWeak] = new WeakReference(this);
asyncHook.enable();
this.on('removeListener', updateExceptionCapture);
diff --git a/src/node_util.cc b/src/node_util.cc
index 5adecf4d9753..c41162dc9a81 100644
--- a/src/node_util.cc
+++ b/src/node_util.cc
@@ -1,5 +1,6 @@
#include "node_internals.h"
#include "node_watchdog.h"
+#include "base_object-inl.h"
namespace node {
namespace util {
@@ -8,7 +9,9 @@ using v8::ALL_PROPERTIES;
using v8::Array;
using v8::Context;
using v8::FunctionCallbackInfo;
+using v8::FunctionTemplate;
using v8::Integer;
+using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::ONLY_CONFIGURABLE;
@@ -172,6 +175,36 @@ void SafeGetenv(const FunctionCallbackInfo<Value>& args) {
v8::NewStringType::kNormal).ToLocalChecked());
}
+class WeakReference : public BaseObject {
+ public:
+ WeakReference(Environment* env, Local<Object> object, Local<Object> target)
+ : BaseObject(env, object) {
+ MakeWeak();
+ target_.Reset(env->isolate(), target);
+ target_.SetWeak();
+ }
+
+ static void New(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ CHECK(args.IsConstructCall());
+ CHECK(args[0]->IsObject());
+ new WeakReference(env, args.This(), args[0].As<Object>());
+ }
+
+ static void Get(const FunctionCallbackInfo<Value>& args) {
+ WeakReference* weak_ref = Unwrap<WeakReference>(args.Holder());
+ Isolate* isolate = args.GetIsolate();
+ if (!weak_ref->target_.IsEmpty())
+ args.GetReturnValue().Set(weak_ref->target_.Get(isolate));
+ }
+
+ SET_MEMORY_INFO_NAME(WeakReference)
+ SET_SELF_SIZE(WeakReference)
+ SET_NO_MEMORY_INFO()
+ private:
+ Persistent<Object> target_;
+};
+
void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context) {
@@ -219,6 +252,16 @@ void Initialize(Local<Object> target,
env->SetMethod(target, "safeGetenv", SafeGetenv);
+ Local<String> weak_ref_string =
+ FIXED_ONE_BYTE_STRING(env->isolate(), "WeakReference");
+ Local<FunctionTemplate> weak_ref =
+ env->NewFunctionTemplate(WeakReference::New);
+ weak_ref->InstanceTemplate()->SetInternalFieldCount(1);
+ weak_ref->SetClassName(weak_ref_string);
+ env->SetProtoMethod(weak_ref, "get", WeakReference::Get);
+ target->Set(context, weak_ref_string,
+ weak_ref->GetFunction(context).ToLocalChecked()).FromJust();
+
Local<Object> constants = Object::New(env->isolate());
NODE_DEFINE_CONSTANT(constants, ALL_PROPERTIES);
NODE_DEFINE_CONSTANT(constants, ONLY_WRITABLE); |
fyi @nodejs/diagnostics |
Deleting from the map on I.e.: index 0caeb624b4..af7db89b34 100644
--- a/lib/domain.js
+++ b/lib/domain.js
@@ -71,6 +71,9 @@ const asyncHook = createHook({
},
destroy(asyncId) {
pairing.delete(asyncId); // cleaning up
+ },
+ promiseResolve(asyncId) {
+ pairing.delete(asyncId); // cleaning up
}
}); |
@ChALkeR I don’t think this is specific to promises :/ And no, I don’t think we can do something inside const async_hooks = require('async_hooks');
async_hooks.createHook({
init(...args) { process._rawDebug('init', args) },
before(id) { process._rawDebug('before', id) },
after(id) { process._rawDebug('after', id) },
destroy(id) { process._rawDebug('destroy', id) },
promiseResolve(id) { process._rawDebug('promiseResolve', id) },
}).enable();
Promise.resolve(42).then(() => { process._rawDebug('inside then') }); prints
|
@addaleax Oh, I missed that (re: call order), thanks. diff --git a/lib/domain.js b/lib/domain.js
index 0caeb624b4..8070610235 100644
--- a/lib/domain.js
+++ b/lib/domain.js
@@ -71,6 +71,11 @@ const asyncHook = createHook({
},
destroy(asyncId) {
pairing.delete(asyncId); // cleaning up
+ },
+ promiseResolve(asyncId) {
+ process.nextTick(() => {
+ pairing.delete(asyncId);
+ });
}
}); Re: not only to promises: can I have a testcase for that? Something timer-related? I might be missing something. Upd: const async_hooks = require('async_hooks');
const end = new Set();
async_hooks.createHook({
init(...args) { process._rawDebug('init', args.join('|')) },
before(id) { process._rawDebug('before', id) },
after(id) {
process._rawDebug('after', id);
if (end.has(id)) {
process._rawDebug('wrong execution order')
process.exit(1)
}
},
destroy(id) { process._rawDebug('destroy', id) },
promiseResolve(id) {
process._rawDebug('promiseResolve', id);
process.nextTick(() => {
end.add(id);
process._rawDebug('promiseResolve (delayed)', id)
})
},
}).enable(); Not sure if that's reliable though, need to look inside |
@ChALkeR I think any GC-based resource would have the same issue – I haven’t tried, but an |
@vdeturckheim I’m not sure I understand the question – the two concepts are very, very different; one is a JS class to create custom async resource types, the other is a function that gives you the currently active resource. |
@rgrove For the record, I've also encountered the same issue this week. Ended up with the same conclusions. To contain the leak, I'm cleaning the active domain (Sentry is the only one using domains in my stack) calling this function at the end of my request handlers: import domain from 'domain'
export default function domainCleanup() {
const activeDomain = domain.active
if (activeDomain) {
const members = activeDomain.members.slice()
members.forEach(member => activeDomain.remove(member))
const eventNames = activeDomain.eventNames()
eventNames.forEach(eventName => activeDomain.removeAllListeners(eventName))
activeDomain.__SENTRY__ = null
delete activeDomain.__SENTRY__
}
} @addaleax The thing is that, using the profiler, I can still see the pairing map referencing each empty domain. I thought that by clearing the members, the request would have been garbaged (which is the case), leading the attached promised to be also collected and, ultimately, clearing the map entry. But that is not what I've observed. Didn't look at |
Ok, so looking at |
Despite what the profiler said, the memory curve stayed flat for 10 hours, so something must collect those domains. |
@addaleax @ChALkeR @mcollina @vdeturckheim Is someone looking into a fix for this? (If not, should we label |
@mmarchini is taking up the fix I started around May and trying to taking over the finishing line. This is going to take a while unfortunately. I'm +1 in getting a failing test in known_issues in the meanwhile. |
@mcollina Are you talking about |
@addaleax +1 on that. |
Based on my tests, you don't even need to attach request object manually to the domain. Just create a promise inside const domain = require("domain");
const app = require("express")();
app.use((req, res, next) => {
let requestDomain = domain.create();
requestDomain.on("error", next);
requestDomain.run(next);
});
app.get("/", (req, res) => {
req.aPromise = new Promise(resolve => resolve("A".repeat(10 * 1024 * 1024)));
res.send("hello world");
});
app.listen(5000); Also not sure why it happens, but |
Avoid circular references that the JS engine cannot see through because it involves an `async id` ⇒ `domain` link. Using weak references is not a great solution, because it increases the domain module’s dependency on internals and the added calls into C++ may affect performance, but it seems like the least bad one. Fixes: nodejs#23862
I’ve opened a PR with the solution from above in #25993 … still not a great solution, though. |
Avoid circular references that the JS engine cannot see through because it involves an `async id` ⇒ `domain` link. Using weak references is not a great solution, because it increases the domain module’s dependency on internals and the added calls into C++ may affect performance, but it seems like the least bad one. PR-URL: #25993 Fixes: #23862 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Refael Ackermann <refack@gmail.com>
Add a simple `WeakReference` utility that we can use until the language provides something on its own. PR-URL: #25993 Fixes: #23862 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Refael Ackermann <refack@gmail.com>
Avoid circular references that the JS engine cannot see through because it involves an `async id` ⇒ `domain` link. Using weak references is not a great solution, because it increases the domain module’s dependency on internals and the added calls into C++ may affect performance, but it seems like the least bad one. PR-URL: #25993 Fixes: #23862 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Refael Ackermann <refack@gmail.com>
Add a simple `WeakReference` utility that we can use until the language provides something on its own. PR-URL: nodejs#25993 Fixes: nodejs#23862 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Refael Ackermann <refack@gmail.com>
Avoid circular references that the JS engine cannot see through because it involves an `async id` ⇒ `domain` link. Using weak references is not a great solution, because it increases the domain module’s dependency on internals and the added calls into C++ may affect performance, but it seems like the least bad one. PR-URL: nodejs#25993 Fixes: nodejs#23862 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Refael Ackermann <refack@gmail.com>
Add a simple `WeakReference` utility that we can use until the language provides something on its own. Backport-PR-URL: #27749 PR-URL: #25993 Fixes: #23862 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Refael Ackermann <refack@gmail.com>
Avoid circular references that the JS engine cannot see through because it involves an `async id` ⇒ `domain` link. Using weak references is not a great solution, because it increases the domain module’s dependency on internals and the added calls into C++ may affect performance, but it seems like the least bad one. Backport-PR-URL: #27749 PR-URL: #25993 Fixes: #23862 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Refael Ackermann <refack@gmail.com>
The following code, which is a reduced test case using functionality that's relied upon by the popular Sentry error reporting module, works fine in Node 8.12.0 but consistently leaks memory on each request in Node 10.12.0, 11.0.0, and 9.11.2:
I would expect the
req
object and itsreq.cachedPromise
property to be GCed once the response is finished, since no references to it are being held. This is what happens in Node 8. But in Nodes 9, 10, and 11,req.cachedPromise
never seems to be GCed, causing a steady memory leak.I first noticed this issue when I tried to upgrade a production service using Sentry to Node 10. It likely affects anyone who uses Sentry with Express and caches promises on the request object (or on any descendant of the request object).
The relevant code in Sentry is here: https://github.com/getsentry/sentry-javascript/blob/839326fbb498c669bd8b9c38bd93d39ca15b6266/packages/node/src/handlers.ts#L220-L229
The text was updated successfully, but these errors were encountered: