-
Notifications
You must be signed in to change notification settings - Fork 3k
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
Finalize is not called at end of pipe, different results with delay #5357
Comments
The same things happen in both StackBlitz examples: When using Note:
// S#1 -> Subscriber#1
src$.pipe(
a(), // S#4
b(), // S#3
finalize(cb), // S#2;
).subscribe(observer) // S#1 - the `observer` is turned into a `Subscriber`
// S#1 is the destination subscriber for S#2 The Now let's see how this applies to your examples. const source = of(2000, 1000).pipe(concatMap(val => process(val)));
function process(val) {
return of(val).pipe(
tap(val => console.log("process-1", val)),
delay(val, queueScheduler),
tap(val => console.log("process-2", val)),
finalize(() => console.log("finalize", val))
);
} When After 2000 ms pass, the Here's what happens when the inner observable completes: protected _complete(): void {
// parent refers to `mergeMap` in this case
this.parent.notifyComplete(this);
// When this happens, the `finalize`'s callback will be invoked as well
this.unsubscribe();
}
notifyComplete(innerSub: Subscription): void {
const buffer = this.buffer;
this.remove(innerSub);
this.active--;
if (buffer.length > 0) {
this._next(buffer.shift()!); // Create an inner obs from the oldest buffered value
} /* ... */
} So, as you can see, when the inner observable completes, it will first notify the parent( If you removed the |
Thx Andrei, in shedding some light in the implementation. I think your note should read: We have a delayWhen, acting as a semaphore, to synchronize that other async tasks have all subscribed. Independent of scheduler used, the behavior will be the same, the inner finalize will be called at next or complete/error of the outer subscription. Making it dependent on the outer subscription and not being the last part run to finish a pipe. This is not expected, many other programming language use a try/catch/finally, or using/destroy structure. The final part is called before continuing to the next block (read next value in RxJS). @ devs When not accepted as a bug, finalize can not be used. As a quick fix, I could move the finalize code to a catchError. As I have a defined end in the expand, I could throw an Error. |
I'd disagree. if (delay > 0) {
return super.schedule(state, delay);
} where IMO, the problem in this case is that the inner observable notifies its parent outer observable about a If you want to run the callback when the complete notification is sent, you can use |
Thanks again Andrei, for your exhaustive answer. About the scheduler, I was referencing: I think, you wanted to point me to something else. Thanks for the tip, this is a "real" finalize, how I expect it to be, and will solve my problem: @ devs |
The repro seems to exhibit expected behaviour, IMO and, FWIW, I don't understand what is meant by this:
|
I expected, and that is also how other programming language implement a finalize is, that finalize is called before leaving a block (RxJS: pipe) and going to a next block (speak reading the next value for concatMap). Without having a delay / delayWhen, it behaves like expected. With them, finalize is called on the next value and on complete/error of the outer subscription. An application can work for a long time, just by adding a delay / delayWhen (or others) the behavior changes. Maybe a function returning an Observable is used and changed, it will not be obvious that it will have an impact on a pipe with a finalize. When you tell me it is as designed, I will never use finalize anymore and use Andrei's tip with tap. I was just lucky that I kept a console.log in finalize, I saw it for a long time, that the two last finalize came at the same time (ending of the outer subscription). Because I didn't had in some branches (if/else) within the expand no console.log, before doubting the correctness of them having two finalize at the same time (and the others be called at the "wrong" time). I would urge you, to add this changing behavior, when adding a delay, to the documentation. EDIT: The last SB in my previous post was not saved, I updated it, showing the side-effect, explanation inside SB |
I think the unexpected behavior will occur whenever you're dealing with observables that don't emit in the same tick they are created. For example: What I'm trying to emphasize is that it's not |
@FritzHerbers I'll have another look at this tomorrow, when I am less tired. I think this is the same issue that was raised in #4138 and #4931 and discussed in #4222
That won't run the callback on explicit unsubscription - which might or might not be what you want - so it's not the same thing. However, it is possible to write a |
@cartant |
@FritzHerbers It's still on my list if things to do. I know exactly what's going on and I'll write the operator tomorrow and explain how it works, etc. |
There is a The difference between it and unsubscribe() {
super.unsubscribe();
const { callback } = this;
if (callback) {
callback();
this.callback = undefined!;
}
} This differs from rxjs/src/internal/operators/finalize.ts Lines 80 to 85 in dfa239d
It's added in the rxjs/src/internal/operators/finalize.ts Lines 70 to 72 in dfa239d
rxjs/src/internal/Observable.ts Lines 212 to 222 in dfa239d
And that means that - unlike in IMO - well, in my changed opinion - this is a bug, but fixing it would be a breaking change. It's also possible that others might not consider this to be a bug. |
[Edited] Based on the feedback from @cartant my issue is not the same as author's, but I guess someone might end up here. I have similar problem too, have a look at following code from StackBlitz:
In my example, the loading starts, data is fetched from the network and with the help of finalize loading stops. I use shareReplay to cache the fetched data. When subscription to source happens, and data arrives, I expect loading to be stopped already. However, as seen in example, finalize hasn't been executed yet and loading is still true at this point in the tap. |
@gogakoreli your's is not the same problem. Your |
@cartant thanks for the feedback. I made this suggestion because if execution is synchronous |
@gogakoreli I see what you mean. The behaviour is surprising when the source is synchronous, but it's unrelated to |
@cartant I was experimenting with it more and looks like the issue still happens without shareReplay. (shareReplay really doesn't seem to matter for the issue, I wanted to just showcase closely my real example) |
@cartant This doesn't look like an issue with In the majority of cases, observables should not be synchronous, however, so this would be a non-issue. Here's what's playing out:
In the case of the first example in the original post, there's a I'm going to close this issue as "expected behavior". And chalk it up to another case of "synchronous observables produce surprising outcomes because they're synchronous". I hope my explanation helps, @FritzHerbers. |
@benlesh From a programmers perspective the "expected behavior" is the one I know from various other programming languages. Not being disrespectful, for me it is "as implemented" or "as designed". That behavior changes when a delay (and others, which was maybe even added in a very deep stream) is added might not be noticed, is a second "misbehavior" of the issue. @cartant made a "dispose" operator, hooking up things differently, would this the way to go for finalize or add "dispose" to rxjs? At the moment we are using the "extended" tap proposed by @Andrei0872, which works just fine and "as expected". We are not interested that the callback runs explicitly on unsubscription, it just needs to be called at the end of the "block". |
I see @cartant ... so it's called during teardown, but we should be calling it last, I agree. The fix for this should be as simple as moving the addition of the callback to the operator It is a breaking change, however I doubt it will break many people because it'll only really be noticeable in synchronous cases, or cases with multiple finalize actions touching shared state in ways that don't behave well if they're out of order. Seems unlikely. |
It's in the As for the change to |
Bug Report
Current Behavior
We noticed that finalize is not called at the end of a pipe, in combination with concatMap.
Reproduction
We could reproduce our production problem when using a delay in the pipe. We are using delayWhen, maybe it has the same side-effect as delay:
https://stackblitz.com/edit/typescript-dhe1y9?file=index.ts
In production we use a very simplified version of the following SB:
https://stackblitz.com/edit/typescript-pf9zt7?file=index.ts
With delay, the finalize is called before next value or on complete.
Expected behavior
Finalize should be called at the end of a pipe, and before a next value is emitted from concatMap.
Environment
The text was updated successfully, but these errors were encountered: