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

fix: finalize after unsubscription from source #5433

Merged
merged 5 commits into from
May 18, 2020

Conversation

cartant
Copy link
Collaborator

@cartant cartant commented May 14, 2020

Description:

This PR changes the implementation of finalize so that the callback passed to the operator is not called until after the subscriber unsubscribes from its source. The effect of this is that - with this change - finalization happens in same order as completion and unsubscription.

Specifically, with unsubscription, we can be sure that when a subscription's unsubscribe method returns, every source observable in the chain will have been guaranteed to have been unsubscribed.

Without this change, finalize's behaviour is very different to that of unsubsciption.

The problem is that when finalize calls its callback, the observable to which it is applied is still subscribed to any sources and any finalize operators applied to sources further up the composed chain will not have called their finalization callbacks.

Aside from this behaviour being surprising, it makes finalize pretty much useless for resource management. For example, if there are observables that use and release some resource:

const a = /* whatever */.pipe(finalize(() => /* release some sub-resource */));
const b = /* whatever */.pipe(finalize(() => /* release some sub-resource */));

and are composed into another observable:

const c = merge(a, b).pipe(finalize(() => /* release the resource */);

the finalize calls will not do what you would expect, as the calls to release the sub-resources in a and b will happen after the release of the resource itself occurs in c and - depending upon the resource - that can effect an error.

Why the call order is weird without this change:

The order in which the callbacks are called is weird because the finalize operator adds the callback to the Subscription:

class FinallySubscriber<T> extends Subscriber<T> {
constructor(destination: Subscriber<T>, callback: () => void) {
super(destination);
this.add(new Subscription(callback));
}
}

It's added in the Subscription constructor, so the callback is the first teardown in the subscription. The subscription to the source is going to be added after that:

call(subscriber: Subscriber<T>, source: any): TeardownLogic {
return source.subscribe(new FinallySubscriber(subscriber, this.callback));
}

const sink = toSubscriber(observerOrNext, error, complete);
if (operator) {
sink.add(operator.call(sink, this.source));
} else {
sink.add(
this.source || (config.useDeprecatedSynchronousErrorHandling && !sink.syncErrorThrowable) ?
this._subscribe(sink) :
this._trySubscribe(sink)
);
}

It's a breaking change:

Although the re-ordering of the finalize calls broke none of the existing tests, this is a breaking change, but it's a change for the better, IMO.

Related issue (if exists): See #5372 and issues linked therein.

@cartant cartant removed the PR: WIP label May 14, 2020
@cartant cartant marked this pull request as ready for review May 14, 2020 11:00
@cartant cartant requested a review from benlesh May 14, 2020 11:00
@cartant
Copy link
Collaborator Author

cartant commented May 14, 2020

I added this test as another example. I doesn't make much sense to me for the finalize callback to be called before the source observable is torn down.

@cartant
Copy link
Collaborator Author

cartant commented May 14, 2020

@benlesh

The fix for this should be as simple as moving the addition of the callback to the operator call instead of in the constructor.

Yeah, that's much better. Thanks.

@benlesh benlesh merged commit 0d7b7c1 into ReactiveX:master May 18, 2020
@lock lock bot locked as resolved and limited conversation to collaborators Jun 24, 2020
@cartant cartant deleted the finalize-later branch September 24, 2020 07:10
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants