▲ | throwitaway1123 2 days ago | ||||||||||||||||
> This isn't actually about removing the promise (completion) listener, but the fact that promises are not cancelable in JS. You've made an interesting point about promise cancellation but it's ultimately orthogonal to the Github issue I was responding to. The case in question was one in which a memory leak was triggered specifically by racing a long lived promise with another promise — not simply the existence of the promise — but specifically racing that promise against another promise with a shorter lifetime. You shouldn't have to cancel that long lived promise in order to resolve the memory leak. The user who created the issue was creating a promise that resolved whenever the SIGINT signal was received. Why should you have to cancel this promise early in order to tame the memory usage (and only while racing it against another promise)? As the Node contributor discovered the reason is because semantically `Promise.race` operates similarly to this [1]:
Assuming `x` is our non-settling promise, he was able to resolve the memory leak by monkey patching `x` and replacing its then method with a no-op which ignores the resolve and reject listeners: `x.then = () => {};`. Now of course, ignoring the listeners is obviously not ideal, and if there was a native mechanism for removing the resolve and reject listeners `Promise.race` would've used it (perhaps using `y.finally()`) which would have solved the memory leak.[1] https://github.com/nodejs/node/issues/17469#issuecomment-349... | |||||||||||||||||
▲ | kaoD 2 days ago | parent | next [-] | ||||||||||||||||
> Why should you have to cancel this promise early in order to tame the memory usage (and only while racing it against another promise)? In the particular case you linked to, the issue is (partially) solved because the promise is short-lived so the `then` makes it live longer, exacerbating the issue. By not then-ing the GC kicks earlier since nothing else holds a reference to its stack frame. But the underlying issue is lack of cancellation, so if you race a long-lived resource-intensive promise against a short-lived promise, the issue would still be there regardless of listener registration (which admittedly makes the problem worse). Note that this is still relevant because it means that the problem can kick in in the "middle" of the async function (if any of the inner promises is long) while the `then` problem (which the "middle of the promise" is a special case of "multiple thens", since each await point is isomorphic to calling `then` with the rest of the function). Without proper cancellation you only solve the particular case if your issue is the latest body of the `then` chain. (Apologies for the unclear explanation, I'm on mobile and on the vet's waiting room, I'm trying my best.) | |||||||||||||||||
| |||||||||||||||||
▲ | GoblinSlayer 2 days ago | parent | prev [-] | ||||||||||||||||
For that matter C# has Task.WaitAsync, so waited task continues to the waiter task, and your code subscribes to the waiter task, which unregisters your listener after firing it, so memory leak is limited to the small waiter task that doesn't refer anything after timeout. |