8000 fix(core): call ngOnDestroy on component level services that are provided using a factory function by llorenspujol · Pull Request #28737 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

fix(core): call ngOnDestroy on component level services that are provided using a factory function #28737

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

Conversation

llorenspujol
Copy link
@llorenspujol llorenspujol commented Feb 14, 2019

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?

ngOnDestroy is not called for Services provided via factory function into component level,

Related Issues: 28738; 22466 (this issue is closed, but it only does it for providers provided at module level, not at component level)

Fixes #28738

What is the new behavior?

ngOnDestroy is called on services that are provided with a factory function at component level

Does this PR introduce a breaking change?

  • Yes
  • [ x] No

Other information

This solution is based on the ones that solved the same issue for the factory function services provided at Module level, see: 5581e97

@llorenspujol llorenspujol requested a review from a team as a code owner Februar 8000 y 14, 2019 18:26
@benlesh benlesh added the area: core Issues related to the framework runtime label Feb 20, 2019
@ngbot ngbot bot added this to the needsTriage milestone Feb 20, 2019
@llorenspujol llorenspujol force-pushed the fix-providers-ngondestroy-bug branch from 8baad5e to 194b75a Compare February 20, 2019 22:56
@llorenspujol llorenspujol changed the title WIP - test(core): Added tests for services that are destroyed at component level fix(core): call ngOnDestroy on component level services that are provided using a factory function Feb 20, 2019
@llorenspujol llorenspujol force-pushed the fix-providers-ngondestroy-bug branch 2 times, most recently from 10e12f5 to 5ed7c8d Compare February 23, 2019 11:26
@distante
Copy link
distante commented Mar 4, 2020

Why was this never reviewed?

@pullapprove pullapprove bot requested a review from mhevery March 4, 2020 22:28
@Rush
Copy link
Rush commented Sep 4, 2020

I have the same question, why is this not reviewed?

Copy link
Contributor
@mhevery mhevery left a comment

Choose a reason for hiding this comment

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

LGTM

Could you rebase this on latest master.

@mhevery mhevery self-assigned this Sep 9, 2020
@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: minor This PR is targeted for the next minor release target: patch This PR is targeted for the next patch release labels Sep 9, 2020
@ngbot
Copy link
ngbot bot commented Sep 9, 2020

I see that you just added the action: merge label, but the following checks are still failing:
    failure conflicts with base branch "master"
    pending status "google3" is pending
    pending status "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 target: major This PR is targeted for the next major release target: minor This PR is targeted for the next minor release labels Sep 9, 2020
@@ -312,6 +311,53 @@ import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetR
});
});

describe('destroy', () => {
Copy link
Member

Choose a reason for hiding this comment

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

These tests only verify VE behavior, not Ivy. Support for this setup should probably also be tested using an acceptance test (e.g. in packages/core/test/acceptance/di_spec.ts), as those run against both renderers. I believe you'll find the Ivy behavior to be different, given that this fix only concerns VE at the moment and Ivy also doesn't seem to call ngOnDestroy for component-level providers.

Copy link
Contributor

Choose a reason for hiding this comment

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

@llorenspujol just wanted to mention that I had a draft/experimental implementation of this change for Ivy, you can find it here. Feel free to use it if needed (it also contains some tests that you can reuse). Thank you.

def.flags |= NodeFlags.OnDestroy;
view.def.nodeFlags |= NodeFlags.OnDestroy;
if (def.parent) {
def.parent.childFlags |= NodeFlags.OnDestroy;
Copy link
Member

Choose a reason for hiding this comment

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

Only updating the flags of the direct parent sounds insufficient to me, as I believe that childFlags is transitive starting from the root node.

Copy link
Member
@JoostK JoostK Sep 9, 2020

Choose a reason for hiding this comment

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

Additionally, once NodeFlags.OnDestroy is set then VE will unconditionally call the ngOnDestroy method:

if (lifecycles & NodeFlags.OnDestroy) {
provider.ngOnDestroy();
}

This introduces a problem, as def is shared across all component instances but different instances could return different service implementations from their useFactory function, so having the NodeFlags.OnDestroy flag set on the shared definition does no longer guarantee that an ngOnDestroy method is actually present on those different service implementations.

Copy link
Author

Choose a reason for hiding this comment

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

@JoostK Can you write some use case where these issues happens? for example a test case, or just a draft... I am unable to reproduce both possible problems that you describe.

For example I am debugging all the process and I don't see childFlags transitive from the root node, I may miss something in the process... So, a use case where this issue happens would be very useful!

Thanks in advance!

Copy link
Member

Choose a reason for hiding this comment

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

@llorenspujol Thanks for looking into it. I'll try provide you with a test later today/tomorrow.

Copy link
Member
@JoostK JoostK Sep 15, 2020

Choose a reason for hiding this comment

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

@llorenspujol Here's the scenarios that would fail in VE but work in Ivy: JoostK@2d6cb9b. I patched the VE implementation to make it work.

(The last of the three tests wouldn't have failed in VE, I added it for good measure)

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the reproductions, you really nailed both:), I was miss understanding the 'childs' meaning a little bit:), now it's clear.

Copy link
Contributor
@mhevery mhevery left a comment

Choose a reason for hiding this comment

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

Those are excellent points @JoostK! Yes at a minimum we should move the tests into acceptance folder to verify that same behavior is in Ivy.

@AndrewKushnir AndrewKushnir removed the action: merge The PR is ready for merge by the caretaker label Sep 9, 2020
@mhevery
Copy link
Contributor
mhevery commented Sep 10, 2020

This PR was discussed in a team meeting and here are the conclusions:

  • We should treat issues which affect ViewEngine in the same way as LTS. Only regressions.
  • If the issue affects both ViewEngine and Ivy, than we should fix both so that we can make migration easier.

So for this PR it has to be refactored by moving the test to acceptance packages/core/test/acceptance/di_spec.ts so that we can ensure that the behavior is consistent between ViewEngine and Ivy.

@mhevery mhevery added the action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews label Sep 10, 2020
@llorenspujol llorenspujol force-pushed the fix-providers-ngondestroy-bug branch 3 times, most recently from 7d2f165 to 6bcd2ce Compare September 15, 2020 16:41
@mhevery
Copy link
Contributor
mhevery commented Nov 16, 2020

Summary

(I have discussed this with @AndrewKushnir and leaving our thoughts here for prosperity)

  1. It seems that calling ngOnDestroy on any injectable is the right thing to do and is consistent behavior across framework. This should include useFactory cases as well which this PR solves.
  2. With useFactory case there is complication that useFactory can return the same instance on multiple invocation:
  • If developer chooses to return same instance, then it is developers responsibility to make sure that ngOnDestroy does something reasonable.
    • There is no easy way for framework to know what "reasonable" action should be if the same instance is returned multiple times.
    • There is no easy/performant way for framework to determine and track that the same instance was returned and somehow delay the ngOnDestroy
    • It would be hard to explain in our documentation the corner cases of returning same instance by useFactory and than describe rules under which the ngOnDestroy will run.

We believe that this PR fixes a bug. The fact that there is code out there which relies on this bug is unfortunate, but this should be treated as a bug fix. We expect that this will have small impact because to see difference in behavior one has to 1) use useFactory 2) return the same instance on multiple occasions, 3) have ngOnDestroy method which invalidates the object for subsequent usage. We don't believe this is a situation which is common enough.

For existing code the simplest way to refactor for the new correct behavior is to change the injectable to have a ref-counter. Every time an instance of injectable is handed out by usFactory the counter is incremented. When framework calls ngOnDestroy the counter is decremented. When count reaches zero, ngOnDestroy performs the cleanup.

@llorenspujol llorenspujol force-pushed the fix-providers-ngondestroy-bug branch 5 times, most recently from de97725 to a1c3f14 Compare November 20, 2020 15:32
@llorenspujol
Copy link
Author

Thanks for the summary, it makes sense.

So for the refactoring to be enabled, I have pushed new changes adding a refCount to AbstractFormGroupDirective. This way both NgModelGroup and FormGroupName useFactory usages can be refactored if needed.

I also have modified the forms test that was breaking to use this refactoring, causing it to work as expected.

Let me know what do you think about this changes,

Thanks!

@mhevery
Copy link
Contributor
mhevery commented Nov 20, 2020

So for the refactoring to be enabled, I have pushed new changes adding a refCount to AbstractFormGroupDirective. This way both NgModelGroup and FormGroupName useFactory usages can be refactored if needed.

Right, so from core point of view. This PR is good. If there are other tests failing, than it is those tests which need to be fixed, most likely through the refCount trick.

@llorenspujol llorenspujol force-pushed the fix-providers-ngondestroy-bug branch from a1c3f14 to 29b7c90 Compare November 23, 2020 07:55
… and are provided at component level (angular#28738)

Fix ngOnDestroy to be called on useFactory providers that implement it
and are provided at component level. Fix applies to both ViewEngine and
@llorenspujol llorenspujol force-pushed the fix-providers-ngondestroy-bug branch from 29b7c90 to fef12c3 Compare November 23, 2020 08:03
@JoostK
Copy link
Member
JoostK commented Nov 23, 2020

@llorenspujol FYI there's currently an issue with CI, so you won't get the tests green at the moment.

Comment on lines +87 to +102
/**
* @description
* Returns the current number of references to this directive.
*/
get refCount() {
return this._refCount;
}

/**
* @description
* Adds a reference into the total reference count.
*/
addRef() {
this._refCount++;
}

Copy link
Member

Choose a reason for hiding this comment

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

Is there are reason for these being public? It seems like an implementation detail to me.

Copy link
Author

Choose a reason for hiding this comment

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

refCount() is an implementation detail but I set it public for consuming it in tests, we could maybe put an underscore to note that it is in fact private.

addRef() Needs to be public AFAIK since we need to be able to execute it when we provide the same instance multiple times in order to correctly do the cleanup.

Copy link
Author

Choose a reason for hiding this comment

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

My preferred solution would be just having a variable _refCount, for example to avoid having those methods on the 'public api', since they are in fact a hacky way to solve one specific problem.

But I was totally unable to run the build using only that variable, don't know why NgModelGroup._refCount was failing... (the error was _refCount is not a property of NgModelGroup). So I 'imagined' there would be some restriction or some step I don't know and I decided to put those methods to make it work.

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe we should revert these changes in Forms based on #28737 (comment). Current understanding is that the problem with re-providing the same instance via useFactory should be taken care of on the apps (not framework) side. However I'd propose to keep a test for now (and mark it with xit vs it) to have more context and we can update or remove the test later.

The next step for this PR is to perform further investigation in Google's codebase (Angular team will handle that) to see if it's feasible to update the code to fix the issue with ngOnDestroy. We'll have more details after investigation and will make a final decision based on this data.

Copy link
Author

Choose a reason for hiding this comment

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

I may miss something but as far as I know without modifying NgModelGroup or AbstractFormGroupDirective with the capability to count it's references, and execute ngOnDestroy only when there is no reference left is the only way to refactor this use case. It still would have to be handled by the apps that consume it, executing ngModelGroup.addRef() whenever the same instance is provided.
Custom providers from apps could handle this problem the way the want, but in the case of NgModelGroup, apps have no way to prevent calling ngOnDestroy on it, as far as I know.

Other option that comes to mind, without touching framework code, would be making some runtime hack on the __proto__ of NgModelGroup, preventing this way the call of ngOnDestroy, but seems slow and too much complicated.

If there is another way to solve this bug (that I don't see) using refCounting and without touching the framework code, I would like to know, since now only this one's comes to my mind.

Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

@llorenspujol we'd like to see whether we can refactor all affected usages in Google's codebase to avoid this pattern altogether (so we don't need to instrument the code with new API or monkey-patch field onto instances). We can reconsider the approach after initial investigation.

@pullapprove pullapprove bot removed the area: core Issues related to the framework runtime label Dec 9, 2020
@ngbot ngbot bot removed this from the v12-candidates milestone Dec 9, 2020
@pullapprove pullapprove bot added area: core Issues related to the framework runtime area: forms labels Dec 10, 2020
@ngbot ngbot bot modified the milestone: Backlog Dec 10, 2020
@AndrewKushnir
Copy link
Contributor

@llorenspujol sorry for the delay. We've discussed this question with the team once again and I'm going to close this PR for now, since landing this change would result in a lot of breakages. We'll keep the original ticket open and look into other possible solutions if we decide to perform some bigger updates to the DI system/API (in which case we can incorporate this behavior). Thank you.

@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 Apr 21, 2022
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 area: forms breaking changes cla: yes risk: medium target: major This PR is targeted for the next major release type: bug/fix
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ngOnDestroy not called for injectables created via factory providers provided at component level
8 participants
0