Skip to content

fix(testability): fix chained http call #20924

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

Closed
wants to merge 1 commit into from
Closed

fix(testability): fix chained http call #20924

wants to merge 1 commit into from

Conversation

lyml
Copy link
Contributor

@lyml lyml commented Dec 10, 2017

Fixes an issue where chained http calls would prematurely call
testability whenStable callbacks after the first http call.

Fixes #209201

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

[x] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] angular.io application / infrastructure changes
[ ] Other... Please describe:

What is the current behavior?

Testability will call the methods queued up with whenStable asynchronously if the macro task queue is empty after an onStable event has fired from ngZone.

This results in the problem described in #20921

What is the new behavior?

Testability will queue an asynchronous check that the macro test queue is empty and then empty the whenStable queue,

Does this PR introduce a breaking change?

[ ] Yes
[x] No

Other information

The reference issue contains a test which is solved with the code fixes, however it is a protractor test dependent on http calls actually being made, currently working on expressing the intended behaviour as a unit test.

Furthermore the change introduces a timer which tests running fakeAsync do not take into consideration, therefore these tests fail. Currently working on disabling testability for those unit tests.

@jasonaden jasonaden added the area: core Issues related to the framework runtime label Dec 11, 2017
Fixes an issue where chained http calls would prematurely call
testability whenStable callbacks after the first http call.

Fixes #20921
@mhevery mhevery added action: merge The PR is ready for merge by the caretaker target: major This PR is targeted for the next major release target: patch This PR is targeted for the next patch release and removed target: major This PR is targeted for the next major release labels Jan 16, 2018
alexeagle pushed a commit that referenced this pull request Jan 17, 2018
Fixes an issue where chained http calls would prematurely call
testability whenStable callbacks after the first http call.

Fixes #20921

PR Close #20924
@alexeagle alexeagle closed this in 7e3f9a4 Jan 17, 2018
chuckjaz added a commit that referenced this pull request Jan 17, 2018
@chuckjaz
Copy link
Contributor

This was reverted by 47b7898 because it failed internal google tests https://sponge.corp.google.com/a1c7ae5d-bbdd-4493-a424-95b95fabfb8c

@chuckjaz chuckjaz reopened this Jan 17, 2018
@chuckjaz chuckjaz added action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews and removed action: merge The PR is ready for merge by the caretaker labels Jan 17, 2018
@lyml lyml changed the title [WIP]fix(testability): fix chained http call fix(testability): fix chained http call Jan 18, 2018
@lyml
Copy link
Contributor Author

lyml commented Jan 18, 2018

Since I can't see what the failed internal tests at google are, is there any input required from me to get this sorted?

@mhevery mhevery added the action: merge The PR is ready for merge by the caretaker label Jan 19, 2018
@ngbot
Copy link

ngbot bot commented Jan 19, 2018

I see that you just added the PR action: merge label, but the following checks are still failing:
    failure forbidden label detected: PR action: cleanup
    pending status "code-review/pullapprove" is pending

If you want your PR to be merged, it has to pass all the CI checks.

If you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help.

@mhevery mhevery removed action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews action: merge The PR is ready for merge by the caretaker labels Jan 19, 2018
mhevery added a commit that referenced this pull request Jan 19, 2018
@mhevery mhevery self-assigned this Jan 23, 2018
@mhevery
Copy link
Contributor

mhevery commented Jan 25, 2018

          Failed: Timed out waiting for asynchronous Angular tasks to finish after 60 seconds. This may be because the current page is not an Angular application. Please see the FAQ for more details: https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular
While waiting for element with locator - Locator: By(css selector, *[name="summaryField"])
ScriptTimeoutError: asynchronous script timeout: result was not received in 60 seconds

// uses a 0 second timeout to check for outstanding tasks we add our 0 second timeout after a
// micro task (which ensures Testability's timeout is run first).
function afterTestabilityCheck(fn: Function): void {
scheduleMicroTask(() => setTimeout(fn));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I bet some tests rely on the fact that it executes in next microTask rather than MacroTask (setTimeout)

Copy link
Contributor Author

@lyml lyml Jan 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, well the base problem is that waiting for next microTask is not enough for specifically the situation with chained http calls.

Presumably there is an alternative fix which instead modifies the XHRBackend or http/httpClient in some manner, perhaps to use the already existing increasePendingRequestCount infrastructure. However the methods are not part of the public testability interface and in the angular codebase these methods are only used in the testability_spec. Which suggests they are intended to be used for testing.

Which direction do you believe would be the right way to move forward? How common are these failing internal tests? Presumably only things which tests testability should care about how testability determines that it is stable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alxhub Can you look into this and recommend direction?

@quanterion
Copy link

Any updates on this? Without this fix Protractor e2e tests with chained http calls fails. And we can't update our app past Angular 5.2.1

@JiaLiPassion
Copy link
Contributor

@mhevery , @lyml , I would like to look into this one.

And I believe the current behavior (before the PR) is correct,
the code https://github.com/lyml/chained-http here.

doChainedHttpCalls() {
    this.initCalls();

    this.http
      .get('https://httpbin.org/delay/1')
      .switchMap(() => {
        this.state = State.afterFirst;
        return this.http.get('https://httpbin.org/delay/1');
      })
      .subscribe(() => {
        this.state = State.afterSecond;
      });

    this.zone.runOutsideAngular(() =>
    this.testability.whenStable(() =>
    this.setStable()));
  }

  doChainedTimeout() {
    this.initCalls();

    Observable.timer(1000)
      .switchMap(() => {
        this.state = State.afterFirst;
        return Observable.timer(1000);
      })
      .subscribe(() => {
        this.state = State.afterSecond;
      });
    this.zone.runOutsideAngular(() => this.testability.whenStable(() => this.setStable()));
  }
  • chainedHttp
  • timer

behave differently , because Observable.timer internally use setInterval which will make ngZone.hasPendingMacrotasks always be true until the observable is completed.
But in chainedHttp case, ngZone.hasPendingMacroTasks will become false when first http finished.
so the chainedHttp sequence will become

  1. 1st http call, macroTask scheduled
  2. 1st http call finished, ngZone stable
  3. 2nd http call scheduled, macroTask scheduled
  4. 2nd http call finished, ngZone stable

I think this is the correct behavior, if we need to consider the chained http call as a single unit, we should provide some other helper method instead of change current Testability behavior.

@quanterion
Copy link

quanterion commented Feb 18, 2018

I think chained http calls is a very common practice, that can't be avoided without rewrite of apps and API they use. Rich use of RXJS in Angular apps promotes huge usage of different concatMap/mergeMap etc calls. Don't waiting on them to complete is nonsense. If we think about timers I suppose we should wait for timers to complete, its totally OK. If timers don't complete they should be excluded from test environment.

@JiaLiPassion
Copy link
Contributor

@quanterion , I think we should provide some other helper method to let ngZone become stable until Observable.subscribe is called. But I don't think we should modify the standard behavior.

@quanterion
Copy link

quanterion commented Feb 19, 2018

@JiaLiPassion As you can see here waitForAngular() in protractor depends on whenStable()
https://github.com/angular/protractor/blob/964baba5eac52452350bf1d29a191558595c5f1b/lib/clientsidescripts.js
So introducing new helper method don't fix e2e test problems in Protractor

And let us think from definition point of view. Is whenStable() should be true when only first wave of pending tasks are finished? Is component really becomes stable after that? Is zone becomes stable if it should immediately run another task? Is view is stable from user point of view if no data is rendered?

From developer point of view you make rich composition of RxJs streams and calls subscribe only once. And It looks weird that whenStable() returns true before subscribe fires and no data is rendered yet.

I do think whenStable() should be a common way to test components with very popular RxJS stream orchestration. May be there should be another function to wait specially for the first wave of microtasks? I.e. drainCurrentMacrotasks that waits only for tasks currently in queue and clearly indicates that it doesn't wait for anything else.

@quanterion
Copy link

angular/protractor#4582

@lyml
Copy link
Contributor Author

lyml commented Feb 19, 2018

@JiaLiPassion You are correct in describing why it doesn't work but it isn't an argument for why it shouldn't work.

The only purpose of the testability api is to tell Protractor when there are no longer any pending asynchronous tasks. That is its only reason for existing.

Nothing inside of angular actually consumes Testability so adding a new testability, let's call it Testability2, would then lead to Testability1 not being consumed. It would also lead to Protractor having to update how they communicate with Angular due to an internal Angular bug.

This pull request does not modify NgZone. It modifies how Testability determines that all pending tasks have been completed. This is needed because Testability does not handle the case of chained http calls correctly, something that is clearly unintended.

@JiaLiPassion
Copy link
Contributor

@lyml , @quanterion , yes, I agree with you guys that test maybe even angular/core should have some better way to handle such case more friendly, and the expect result in #20921 is perfectly making sense and is a common use case.

What I want to discuss is should we consider such case (such as chained http) to be equals to whenStable.

  • consider the definition of what is the pending tasks, about task, for ngZone. The tasks include
  • microTask, such as promise.then or other microTask scheduled by ngZone which will run before all pending macroTasks.
  • macroTask, such as setTimeout, setInterval, XHR.

So for ngZone, it will only manage the microTask and macroTask listed above, when there is no pending microTasks and macroTasks, ngZone will consider itself stable.

  • Consider the timing of whenStable. I think design is the whenStable should be emitted when no pending tasks and trigger a tick to do change detection.

so for the case here.

this.http
      .get('https://httpbin.org/delay/1')
      .switchMap(() => {
        this.state = State.afterFirst;
        return this.http.get('https://httpbin.org/delay/1');
      })
      .subscribe(() => {
        this.state = State.afterSecond;
  });

The sequence is

  • 1st http call, macroTask scheduled
  • 1st http call finished, ngZone stable
  • 2nd http call scheduled, macroTask scheduled
  • 2nd http call finished, ngZone stable

it is really ngZone become stable twice not once.

So I think maybe we should have some other status such as pending observables which will wait for macroTasks.

@quanterion
Copy link

@JiaLiPassion Your considerations makes a lot of sense but I think sequence is not correct.

public click() {
  this.http
    .get('https://httpbin.org/delay/1')
    .switchMap(() => {
      this.state = State.afterFirst;
      return this.http.get('https://httpbin.org/delay/1');
    })
    .subscribe(() => {
      this.state = State.afterSecond;
  });
}

The sequence is

  • Button clicked
  • 1st http call, macroTask scheduled, ngZone stable
  • 1st http call finished, ngZone stable
  • 2nd http call scheduled, macroTask scheduled
  • 2nd http call finished, ngZone stable

From this point of view the zone is really stable not twice, but three times. There should be no difference between 1st http call sheduled and 2nd http call sheduled

@lyml
Copy link
Contributor Author

lyml commented Feb 19, 2018

@JiaLiPassion Nobody is talking about modifying NgZone. It is the Testability implementation that is being fixed. There are two different APIs here which has a stable event.

NgZone.onStable an observable which should fire if there are no pending microtasks (does not care about macrotasks at all).

Testability.whenStable which takes a callback that should fire when there are no more pending asynchronous tasks. NgZone.onStable will fire multiple times before Testability.whenStable fires.

In the current implementation Testability.whenStable schedules a microtask after each NgZone.onStable event, it uses this microtask to check if all pending macro tasks are completed and in that case it incorrectly marks the environment as stable. However, for pending http requests it is not enough to wait for a microtask.

Therefore the testability implementation is not working properly (which leads to the bugs reported in Protractor).

@JiaLiPassion
Copy link
Contributor

@lyml ,

NgZone.onStable an observable which should fire if there are any pending microtasks (does not care about macrotasks at all).

Testability.whenStable which takes a callback that should fire when there are no more pending asynchronous tasks. NgZone.onStable will fire multiple times before Testability.whenStable fires.

Thank you for explanation, you are right, I mixed up the NgZone.onStable with zone.js onHasTask.
I understand your point, and I know your PR is about Testability.whenStable.

So I may misunderstanding the issue.

in my understanding, with the code

this.http
      .get('https://httpbin.org/delay/1')
      .switchMap(() => {
        console.log('after first isStable', this.zone.hasPendingMacrotasks, this.zone.hasPendingMicrotasks);
        this.state = State.afterFirst;
        return this.http.get('https://httpbin.org/delay/1');
      })
      .subscribe(() => {
        console.log('after second isStable', this.zone.hasPendingMacrotasks, this.zone.hasPendingMicrotasks);
        this.state = State.afterSecond;
  });

So about this case, after first http call, there is no pending macroTasks or microTasks, in current Testability._runCallbacksIfReady implementation, it will check isStable() first, then schedule a microTask to trigger whenStable, at that point , I think there is no macroTask to wait.

And the issue this PR want to resolve is the microTask which scheduled by Testability._runCallbacksIfReady will trigger an additional whenStable callback?

jbogarthyde pushed a commit to jbogarthyde/angular that referenced this pull request Feb 23, 2018
Fixes an issue where chained http calls would prematurely call
testability whenStable callbacks after the first http call.

Fixes angular#20921

PR Close angular#20924
jbogarthyde pushed a commit to jbogarthyde/angular that referenced this pull request Feb 23, 2018
@quanterion
Copy link

Any updates on this issue?

@ngbot
Copy link

ngbot bot commented Mar 14, 2018

Hi @lyml! This PR has merge conflicts due to recent upstream merges.
Please help to unblock it by resolving these conflicts. Thanks!

@JiaLiPassion
Copy link
Contributor

JiaLiPassion commented Mar 23, 2018

@lyml , I believe for this case, the underlying reason of the chained http trigger multiple zone stable call is not Testability, the real reason is the way that how zone.js patch xhr. the detailed information can be found here, angular/zone.js#1055.

currently zone.js patch xhr as a macroTask and after xhr.send, zone.js will add a readystatechange listener, when readystate=4, the macroTask will be invoked.

this logic work fine, but because xhr can also add onload or addEventListener('load', listener).
which is exactly @angular/common/http does, https://github.com/angular/angular/blob/master/packages/common/http/src/xhr.ts#L302
and the load event handler will run after readystatechange, so in the load event handler, the > zone may have been stabled, and cause some issues. in this case, the 2nd http.get is run in load > event handler, so the zone has already stabled, so the zone.onStable was triggered twice.

So in the zone.js PR 1055, I just make sure all onload is finished, then finally invoked the internal readystatechange task.

I have created a fork based on your repo, https://github.com/JiaLiPassion/chained-http, with my zone.js fix, everything work fine, I will add more test cases to verify my idea.

@mhevery, please review, thank you!

leo6104 pushed a commit to leo6104/angular that referenced this pull request Mar 25, 2018
Fixes an issue where chained http calls would prematurely call
testability whenStable callbacks after the first http call.

Fixes angular#20921

PR Close angular#20924
leo6104 pushed a commit to leo6104/angular that referenced this pull request Mar 25, 2018
@jasonaden jasonaden added this to the needsTriage milestone Jan 29, 2019
@mhevery mhevery removed their assignment Jul 26, 2019
@petebacondarwin
Copy link
Contributor

@JiaLiPassion - do your fixes to zone.js mean that this PR is no longer needed?

@JiaLiPassion
Copy link
Contributor

@petebacondarwin , yes, this has been fixed in the zone.js repo, so this one can be closed.

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Feb 20, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: core Issues related to the framework runtime cla: yes target: patch This PR is targeted for the next patch release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants