-
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
concatAll() unexpectedly overlaps inner Observable subscriptions #3338
Comments
As an aside, for the meantime, does anybody have any idea how to obtain the desired behavior (truly serialized inner observables) without having to write my own version of |
This is interesting q, while our operator does guarantee about order of emission for Put aside of debating if it's expected or not, achieving desired behavior seems somewhat tricky though.
|
I agree that one might not view this behavior as a "bug" with regard to the combined emissions of the inner observables, since the emissions still occur as expected, independent of any subtleties in the subscribe/unsubscribe order of the inner observables. That said... in cases where the only thing that matters about the inner observables is their execution (esp. if they represent tasks, or periods of time, etc. have no emissions, and only complete/error), there is strong case to be made that the operation of "concatenation" should actually take care to execute the inner observables in a truly "back to back" fashion. So, I still find it "surprising", and quite unintuitive really, that the subscription of the next observable is overlapped with the teardown of the previous. Certainly, in my case, I wrote a bunch of code (my task queue) thinking that Per my experiment, the subscribe/unsubscribe order within And, if that choice was indeed arbitrary, and could actually be re-made more purposefully, then choosing to explicitly avoid overlapping subscriptions within I hope that change can be considered. |
I'll leave other chime in for opinions, but my personal opinion is still inner subscription management is internals of operator implementation and it's not expected to behave in some way. (Still, it's personal though) Again put aside, I feel like this issue's crux is you'd like to queue certain task in given order, including it's teardown - am I understanding right? in those case, have you considered to use
you'll get
since teardown will execute on completion of each notification metadata. |
Thanks for the idea. Seems like it could work. One note: The inner observables may not always complete "cleanly" though, so error would also have to be "hooked" for this work. Still, this feels quite unintuitive, and unergonomic to have to resort to such esoteric interfaces ( |
this doesn't matter, cause metadata allows you to access either error or complete, (or next value if needed) without blow up observable chain.
for this I've expressed my opinion sufficiently, will leave other member's opinion around. |
Indeed, if I could already "guarantee inner observables are not overlapping when emitted", then I would not need a queue. |
yeah, ignore that - I've already backed out those. |
I vaguely recall discussed about some operator's subscription behavior in last core meeting, maybe these operator is one of the cases? /cc @benlesh |
An alternative solution might be to provide another operator, perhaps named |
Also note that you do (somewhat unexpectedly ;) get the expected output when queuing up synchronous tasks. That is, make the following small change in my original example code: function Task(name) {
return (
Observable
.defer(() => {
console.log(`begin task ${ name }`);
return (
Observable
.of(`simulated work`)
// .delay(100) <== remove the asynchronous element
.finally(() => {
console.log(`end task ${ name }`);
})
);
})
);
} ...and the output is as expected. This is another way in which the current behavior of |
Lastly, if it provides any motivation, or consideration of this behavior as a bug, note that I see the expected output from my repro code in all cases (discussed so far) when running with RxJS 4.x. I have added an "EDIT" about this to the original issue description above. |
It's because the
To get the behavior you want, you'll need to use We could look into triggering the next subscription in |
I think this is a bug. @jayphelps, this is actually a use-case for the marble changes we were discussing. We need the ability to test that we're synchronously unsubscribing from one observable and THEN subscribing to the next in our tests. |
@benlesh do we have guarantee around innersubscription overlapping in operators? |
@kwonoj we've never offered a "guarantee", but I think we need to start, because I agree this is a bug. |
I'm still bit on the fence if this is considered as a bug, instead of |
it's a really minor bug if it's a bug, honestly. |
But you're right, @kwonoj ... ugh... maybe it's not a bug per say. One of the reasons that older versions of Rx didn't have this issue is they were all scheduling via a queue, I think. Which we're not doing for performance reasons. In fact, looking at this, I'm not sure we can make this behave the way it's desired without queue scheduling, which is a non-starter. I'll have to think about it. |
@benlesh wrote:
Depends on your use case. It breaks using Rx to represent mutually exclusive tasks in the most intuitive way (IMO). For me, that is a major bug. With other operators that draw parallels to the world of "tasks", such as As I mentioned earlier, the fact that Otherwise, the high-level Observable lifecycle management afforded by otherwise intuitive subscription and disposal entry-points ( |
@benlesh wrote:
Simply reversing the logic (as I did in my "experiment" mentioned above) is not sufficient? Perhaps I don't understand some other internal implementation detail, but if the unsubscribe is always executed first (which I assume will always execute to completion synchronously), before sending the notification of completion or error, how can the unsubscribe (aka, disposal logic) ever overlap with the following subscribe (aka, initialization logic)? |
No... This is simply how This is really edge casey though.l Until then, just use |
@benlesh, I think something got confused. Sorry, when I said:
...I was not referring to how things currently work. I was referring to the experiment where I simply reversed the logic within This change seemed to cleanly restore the expected behavior and, logically, appears to make more sense as an implementation choice. Would not such a change be sufficient to guarantee "serialized" subscription behavior for (The experimental code change appears in my original issue description above.) |
@benlesh, if you agree with my last comment, can we consider getting this issue marked as a bug? |
@mattflix I'm sorry, I don't quite follow your last two comments. Do you have a PR I can review? |
(I was referring, overall, to our exchange in the prior 5 comments in this issue, which were only between you and me, all on 2/23.) I don't have a PR. I could try to create one, but I am not knowledgeable enough to author any tests, or whatever else, for a "complete" PR in this repro. That said, the change I am proposing, or at least just trying to point out, is trivial. I literally just reversed the lines that are (apparently) responsible for the ordering "subscribe -> unsubscribe" between all the Observables handled by The description of this issue includes a snippet from Does what I am proposing/referencing not solve the issue? (Again, I am not knowledgeable enough to know if such as change will and will not have other ramifications, or how to verify that. But, it seems simple enough, and logically correct, to me.) |
@benlesh, this thread seems stalled. Any thoughts wrt to my last comment on Feb 26? |
So I just ran into this issue. I have a list of Observables that need to do some cleanup in a finally block before the next one can run. I cannot use Observable.concat to run these Observables in sequence, because the first Observable hasn't cleaned up when the next one is run. @benlesh I don't understand what you are suggesting when you say "Until then, just use tap or do's complete or error handlers to execute whatever logging/side-effect you wanted to do". Right now I am at a loss how I should work around this issue. Don't know how often people run in to this issue, but this is definitely a major issue for me right now. It is stopping me from doing something I need to do. |
Ok figured out one workaround, which is to use observeOn to revert to the rxjs 4 behavior as ben describes using another scheduler.
|
@samal84, yeah, I figured out the same Another workaround is to insert "dummy" Observables between the "real" (in my case, the serialized Tasks) Observables that simply introduce an artificial asynchronous delay. Again, this is a hack and feels totally unnecessary. @benlesh, can you weigh-in on whether this issue can be considered as bug? |
@samal84 @mattflix I came across this issue when reading through old issues to see whether any could be closed. A slightly better alternative to |
Thanks for the tip @cartant ! I had the same concern, so I made this deviant that I am currently using:
Good to know that subscribeOn will have the same effect. Less lines for me to maintain :) |
@samal84
Deleting code is the best! |
… next Resolves an issue where inner observables would not finalize before the next inner observable got subscribed to. This happened in concat variants, and merge variants with concurrency limits, and could be surprising behavior to some users. fixes ReactiveX#3338
I finally have a fix for this (and all concat variants) and it will likely land in v7: #6010 |
EDIT: Note that the following repro code always produces the expected output with RxJS 4.x. The output with RxJS 5.x is unintuitive (IMO) and can actually vary depending on the formulation of the inner observables (see further discussion of such cases in subsequent comments).
RxJS version: 5.5.0 (and all previous 5.x versions, seemingly)
Code to reproduce:
Expected behavior:
Actual behavior:
Additional information:
I discovered this behavior when using
concatAll()
as the basis for a serialized "task queue" for executing tasks (represented asObservables
) one at a time, much like my (simplified) sample code above. Some of these tasks would allocate resources indefer()
(or similar manner) and release them infinally()
(or similar manner).My task queue implementation all seemed to make logical sense, yet I was running into errors caused by concurrent access to resources that were only supposed to be accessed in a mutually-exclusive manner, but somehow weren't. I was stumped, I couldn't see the the problem in my task queue or my tasks that was leading to this unexpected concurrent access.
After much head-scratching and debugging, I eventually tracked the problem to the
InnerSubscriber
implementation within rxjs, which contains logic that subscribes to the next sequence before unsubscribing from the previous sequence:Ah-ha.
This causes the "creation" logic (
defer()
callback or other invocation) of the next sequence to execute before the "disposal" logic (finally()
or other invocation) of the previous sequence has executed, making it impossible for the inner observables managed byconcatAll()
to behave like serialized tasks -- since rxjs actually "overlaps" the tasks at this critical moment.As an experiment, I simply reversed the logic:
Then, using the original "Code to reproduce" above, I actually see the expected behavior.
I don't know, however, what other ramifications this change might have on other use cases. In the case of
concatAll()
(and variants) however, it would definitely seem "more correct" and "less surprising" for rxjs to always unsubscribe from the previous sequence before subscribing to the next.The text was updated successfully, but these errors were encountered: