Skip to content
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

SafeSubscriber does not unsubscribe itself #2675

Closed
czterocyty opened this issue Jun 17, 2017 · 18 comments
Closed

SafeSubscriber does not unsubscribe itself #2675

czterocyty opened this issue Jun 17, 2017 · 18 comments

Comments

@czterocyty
Copy link

czterocyty commented Jun 17, 2017

When subscribing to Observable with Partial Observer, rxjs creates SafeSubscriber, but on unsubscribe it does not unsubscribe itself. Moreover if a closure is passed to next/error callback of partial observer, this leads to memory leaks, as consumers cannot be freed.

See the code (pure JS).

RxJS version: 5.4.1

Code to reproduce:

via gist: https://gist.github.com/czterocyty/d4ad5dea55316c9a734c64f378975f04

Expected behavior:

I think SafeSubscriber#unsubscribe should be called too upon unsubscription.

Test should pass:

  it('upon subscription, the destination SafeSubscriber should be unsubscribed too', () => {
    const observer = {
      next: function () { /*noop*/ },
      error: function (e) { throw e; }
    };

    const sub = new Subscriber(observer);

    const destination = sub['destination'];
    const destUnsubscribe = destination['unsubscribe'] = sinon.spy();
    const destUnsubscribeProtected = destination['_unsubscribe'] = sinon.spy();

    sub.unsubscribe();

    expect(sub.closed).to.eq(true);
    expect(destination.closed).to.eq(true);

    expect(destUnsubscribe).have.been.called;
    expect(destUnsubscribeProtected).have.been.called;
  });

Actual behavior:

It is not when passing partial observer into Subscriber.

Additional information:

After playing around with buttons, check JS heap and not-freed Foo instance due to handing closures in SafeSubscriber. See https://drive.google.com/open?id=0B4JH51FD7JBZcUJTTVRjRXJ2a00. There is Subscriber which is closed (good), but destination as SafeSubscriber is still not closed.

@jayphelps
Copy link
Member

@trxcllnt is this the same unsubscribe issue as what you and I were discussing on Slack? I think so.

@martin-the-geek
Copy link

Any work being done on this. Just noticed a memory leak in my angular application because of this.

@volser
Copy link

volser commented Nov 18, 2017

Any news? I have the same issue (

@Medo42
Copy link

Medo42 commented Feb 15, 2018

I just spent several hours of debugging until identifying this issue. It caused our application to never unsubscribe from an observable that fired a request every few seconds, so as the user kept using the application the frequency of requests would go up and up.

So +1 on this issue, please give this more priority.

@Medo42
Copy link

Medo42 commented Feb 19, 2018

I am not completely sure the issue I saw is the same as this one, since I don't understand the internal workings of rxjs well enough. However, for me the bug seems to have been introduced here: a1778e0#diff-c377d3777a1fedd1d4744b0bb9eb939cL170

SafeSubscriber used to patch the unsubscribe method of the PartialObserver, so that if the PartialObserver was unsubscribed from, the SafeSubscriber would unsubscribe up the chain as well. The change in that linked commit still patches unsubscribe on the context object, but that is no longer the same object as the original PartialObserver, so an unsubscribe on the original object no longer propagates.

In my case, this causes the unsubscribe on the InnerSubscriber of switchMap to not unsubscribe "up the chain", so the TimerObservable keeps firing and the chain keeps firing requests to our server even though really nobody is subscribed anymore. Normally this would not happen, because InnerSubscriber (as a Subscriber) would cause a different code path to be taken where the unsubscription is handled properly. However, in my case the switchMap and its InnerSubscriber come from a different copy of the rxjs library than the Observable that it maps, so it does not extend the same Subscriber class and is treated as a PartialObserver. I know that this is not an ideal situation, but IMO it should not cause a difference in behavior, because from a library consumer point of view, the same interfaces are being adhered to.

I have some trouble figuring out how it should work though, because the whole approach of chaining Subscribers seems incompatible with that case. If you have a general PartialObserver of unknown implementation, I simply see no way to react to its unsubscribe, because we don't even know if it has one.

Edit: The multiple identities of Subscriber seem to be caused by different forms of import, see #2984

@cghislai
Copy link

cghislai commented Apr 4, 2018

Still happening with recent versions of rxjs 5. this is rather severe in angular applications as component instances are not garbage collected if they leak subscriptions.

@GuichiZhao
Copy link

+1

2 similar comments
@nanoTitan
Copy link

+1

@perfilyev
Copy link

+1

@BioPhoton
Copy link
Contributor

@czterocyty and @jayphelps.
IMHO this issue is already solved in the latest version.
If I'm right, please any of u 2 could close the issue.
Otherwise please ignore my comment.

@jayphelps
Copy link
Member

@BioPhoton FWIW looking back on my original reply, it appears that the latest version of RxJS still does not call the unsubscribe() callback when unsubscribe() is called on the subscription. i.e. nothing has changed, in regards to my particular issue. Also whether or not what I was asking for is something we want, I'm not sure anymore as I've lost context of why I cared.

RxJS v6: https://jsbin.com/gibozogero/1/edit?html,js,console

@trxcllnt is this the same unsubscribe issue as what you and I were discussing on Slack? I think so.

Whether it was in fact the same as what the OP had an issue with, I'm not sure.

@startswithaj
Copy link

This is still happening.

@cartant
Copy link
Collaborator

cartant commented Feb 13, 2020

(For my own reference) this issue is somewhat related to this one: #5243

trxcllnt added a commit to trxcllnt/rxjs that referenced this issue Feb 13, 2020
Ensure the internal SafeSubscriber is disposed when an Observable is subscribed with a Subscriber or Subscriber-like object.

Fixes ReactiveX#2675
@trxcllnt
Copy link
Member

Here ya go everybody #5311

@trxcllnt
Copy link
Member

@Medo42 If you have a general PartialObserver of unknown implementation, I simply see no way to react to its unsubscribe, because we don't even know if it has one.

Yeah that's a problem. My fix in #5311 should address this if the unknown Subscriber-like (or otherly-imported Subscriber) is the terminal Subscriber in the chain.

We could add more robust checks than instanceof Subscriber to determine if the Subscriber-like should be treated like one of our own (checking for add, remove, unsubscribe, etc. methods), but I worry that could fail in other ways.

@Medo42
Copy link

Medo42 commented Feb 13, 2020

Thank you very much for looking into this.

I'm very hazy on the details at this point and haven't even used rxjs in a while, so sadly I can't be of much help. I still wonder why rxjs uses this "subscriber" model internally though, instead of more "naively" implementing the observable/observer interfaces.

@trxcllnt
Copy link
Member

At the time the approach we took was based on throughput benchmarks. Some things are better now with turbofan, but in 2015/16 dynamic/duck-typed object property lookups were expensive.

benlesh added a commit that referenced this issue Jun 9, 2020
* test: add failing interop subscriber test

* fix: chain safe subscribers to interop subscribers

Closes #5469 #5311 #2675

* refactor: remove subscribeWith

* refactor: minor change to types to clean up code.

Co-authored-by: Ben Lesh <[email protected]>
@cartant
Copy link
Collaborator

cartant commented Mar 14, 2021

Closed by #5472 IIRC

@cartant cartant closed this as completed Mar 14, 2021
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

No branches or pull requests