Skip to content

fix(Subscriber): handle memory leaks#5934

Closed
MaximSagan wants to merge 1 commit intoReactiveX:masterfrom
MaximSagan:fix/subscriber-memory-leaks
Closed

fix(Subscriber): handle memory leaks#5934
MaximSagan wants to merge 1 commit intoReactiveX:masterfrom
MaximSagan:fix/subscriber-memory-leaks

Conversation

@MaximSagan
Copy link

Description:
Fixes memory leaks occurring when instances of Subscriber are kept around in memory even after unsubscription (which includes complete() and error()).

Related issue (if exists):
#5931

@MaximSagan MaximSagan force-pushed the fix/subscriber-memory-leaks branch from a6d0164 to 8058b46 Compare December 15, 2020 03:52
@MaximSagan
Copy link
Author

@benlesh Hi.

Copy link
Member

@benlesh benlesh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing is for sure, we don't want to change the semantics of the test I commented on. Overall I see what's being done here and it makes sense.

I think we really want to test destination to null though, and then maybe even have a test that confirms that is the case via introspection? The test wouldn't be super valuable, other than to serve as a reminder that we want to be mindful of that.

bufferWhen(() => {
if (i === 1) {
throw 'error';
return throwError(() => 'error');
Copy link
Member

@benlesh benlesh Jan 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This completely changes the test. Revert.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benlesh Yeah I assumed this wasn't the right way to go about it, but did it on the off-chance that the test itself was previously incorrect (as I figured that bufferWhen's closingSelector was supposed to return an observable).

if (!this.closed) {
this.isStopped = true;
super.unsubscribe();
this.destination = NOOP_OBSERVER;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at the point we've unsubscribed, it seems like we could just set this.destination = null! here, although I haven't tried that and tested.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe simply nulling it out caused several tests to fail.

/**
* The observer used as a stub for subscriptions that have already been closed.
*/
export const NOOP_OBSERVER: Readonly<Observer<any>> & { closed: true } = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably remove this if we're just nulling out destination.

}

const { initialTeardown } = this;
this.initialTeardown = undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use null! here, I think (not sold on it), just because null tells me that we set it, rather than it just never being set.

@benlesh
Copy link
Member

benlesh commented Jan 11, 2021

Okay, I'm looking more into this. It would be great to have a reproduction that didn't involve Angular. But I'm totally certain that the code here isn't the right solution. Using the "noop observer" that this adds causes errors we'd like to propagate somehow to simply be swallowed.

Again, I think I see what you're getting at, but it's hard without a minimal reproduction, and it's also plausible that this is something that only has to do with the shareReplay implementation (which indeed intentionally keeps a subscription around by default by design, for better or worse)

Noteworthy: EMPTY_SUBSCRIBER and null! are not the right solutions here either.

I'm moving back to trying to find a minimal reproduction for this first. That should be our starting point.

cc @cartant

@benlesh
Copy link
Member

benlesh commented Jan 11, 2021

I'm going to close this for now, because I don't want to send the wrong signal to the author, @MaximSagan, to continue working on it. What we really need right now is a minimal, RxJS only, reproduction that definitively shows the leak. I'll try to figure that out too.

@benlesh benlesh closed this Jan 11, 2021
@benlesh
Copy link
Member

benlesh commented Jan 11, 2021

Also to be clear: I'm not saying that there isn't a leak. I'm saying that we need to more definitively identify the cause of the leak we think exists rather than blindly address the symptom.

@MaximSagan
Copy link
Author

@benlesh Thanks for looking into this. Not offended that you're not going with my solution, haha. Really just wanted to get this issue on the radar, and I figured a PR might help.
I'll prepare an RxJS-only reproduction shortly.

@benlesh
Copy link
Member

benlesh commented Feb 10, 2021

@MaximSagan one thing to look for when trying to figure out what is or isn't cleaning up is tests with an expectSubscriptions check, especially WRT inner observables. If you think you see a memory leak, it's easiest to prove that, within RxJS by adding checks to make sure inner subscriptions are unsubscribed at the proper time.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants