question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

concatAll() unexpectedly overlaps inner Observable subscriptions

See original GitHub issue

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:

const { Observable } = require(`rxjs`);

function Task(name) {
  return (
    Observable
      .defer(() => {
        console.log(`begin task ${ name }`);
        return (
          Observable
            .of(`simulated work`)
            .delay(100)
            .finally(() => {
              console.log(`end task ${ name }`);
            })
          );
      })
  );
}

Observable
  .of(
    Task(`1`),
    Task(`2`),
    Task(`3`)
  )
  .concatAll()
  .subscribe();

Expected behavior:

begin task 1
end task 1
begin task 2
end task 2
begin task 3
end task 3

Actual behavior:

begin task 1
begin task 2
end task 1
begin task 3
end task 2
end task 3

Additional information: I discovered this behavior when using concatAll() as the basis for a serialized “task queue” for executing tasks (represented as Observables) one at a time, much like my (simplified) sample code above. Some of these tasks would allocate resources in defer() (or similar manner) and release them in finally() (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:

    InnerSubscriber.prototype._error = function (error) {
        this.parent.notifyError(error, this); // <== subscribes to the next sequence
        this.unsubscribe();
    };
    InnerSubscriber.prototype._complete = function () {
        this.parent.notifyComplete(this); // <== subscribes to the next sequence
        this.unsubscribe();
    };

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 by concatAll() to behave like serialized tasks – since rxjs actually “overlaps” the tasks at this critical moment.

As an experiment, I simply reversed the logic:

    InnerSubscriber.prototype._error = function (error) {
        this.unsubscribe(); // <==  unsubscribe from previous sequence first!
        this.parent.notifyError(error, this);
    };
    InnerSubscriber.prototype._complete = function () {
        this.unsubscribe(); // <== unsubscribe from previous sequence first!
        this.parent.notifyComplete(this);
    };

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.

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:1
  • Comments:34 (18 by maintainers)

github_iconTop GitHub Comments

1reaction
benleshcommented, Feb 9, 2021

I finally have a fix for this (and all concat variants) and it will likely land in v7: https://github.com/ReactiveX/rxjs/pull/6010

0reactions
cartantcommented, Feb 5, 2019

@samal84

Less lines for me to maintain

Deleting code is the best!

Read more comments on GitHub >

github_iconTop Results From Across the Web

concatAll emits multiple observables and is not subscribing ...
This is happening because your map() operator returns Promise[] which means it return an array of Promises. concatAll() takes the array as ...
Read more >
concatAll - Learn RxJS
Collect observables and subscribe to next when previous completes. ... backpressure when the source emits at a faster pace than inner observables complete!...
Read more >
Simple Example for RXJS Flattening Operators
I have first subscribed to the outer observable. The result will be the inner observable returned by innerObsv(). Now again I need to...
Read more >
RxJs Mapping: switchMap vs mergeMap vs concatMap ...
Let's call this new httpPost$ Observable the inner Observable, as it was created in an inner nested code block. Avoiding nested subscriptions.
Read more >
Observable | RxJS API Document
bindCallback(iCallMyCallbackSynchronously, null, Rx.Scheduler.async); boundSyncFn().subscribe(() => ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found