Skip to content
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

feat(core): warn if an aspect was added via another aspect #8639

Merged
merged 7 commits into from
Aug 3, 2020

Conversation

NetaNir
Copy link
Contributor

@NetaNir NetaNir commented Jun 18, 2020

Updated PR

Since we introduced stages which had the unintended side effect of the CDK not supporting adding an Aspect via another Aspect, we have not seen any issues reported beside #8536, that was resolved without requiring this capability.
Given that, and the fact this features has many sharp edges we decided to leave it unsupported, and add a warning in the meantime.


Following up on #8536

If an aspect is added via another aspect, the inner aspect will not be invoked. Take for example this code:

const app = new cdk.App();

app.node.applyAspect({
  visit(construct: cdk.IConstruct) {
    construct.node.applyAspect({
      visit(construct: cdk.IConstruct) {
        console.info("Invoking aspect on construct: " + construct.node.id);  // This will not be called
      }
    })
  }
});

Since aspects are added only on the top level node, if an aspect is added while InvokeAspects is called on that node, it will be ignored since it will not be added to list of aspects to invoke (allAspectsHere in the bellow code):

function invokeAspects(root: IConstruct) {
  recurse(root, []);

  function recurse(construct: IConstruct, inheritedAspects: constructs.IAspect[]) {
    // hackery to be able to access some private members with strong types (yack!)
    const node: NodeWithAspectPrivatesHangingOut = construct.node._actualNode as any;

    const allAspectsHere = [...inheritedAspects ?? [], ...node._aspects];

    for (const aspect of allAspectsHere) {
      if (node.invokedAspects.includes(aspect)) { continue; }

      aspect.visit(construct); <-- an aspect that was added here will not be added to `allAspectsHere` and will be ignored

      node.invokedAspects.push(aspect);
    }

    for (const child of construct.node.children) {
      if (!Stage.isStage(child)) {
        recurse(child, allAspectsHere);
      }
    }
  }
}
 

Assuming this is not something we want to support**, we can detect it by comparing the size of node._aspects before and after the call to aspect.visit, and emit a warning if there has been a change. Note that while the aspect will not be invoked it will be added to every child construct during the recursive visit. Emitting a warning for each child construct will result in a noisy console, to prevent this I have added a flag that will only allow adding one warning per application, given this limitation I'm not sure there is a lot of value in adding the warning, thoughts?

If we decide to add it I will add tests.

(** theoretically we could support it by adding the aspects to allAspectsHere during the loop, but this will require a non trivial implementation in order to avoid infinite recursion)


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@NetaNir NetaNir requested review from eladb and rix0rrr June 18, 2020 23:58
@github-actions github-actions bot added the @aws-cdk/core Related to core CDK functionality label Jun 18, 2020
@mergify mergify bot added the contribution/core This is a PR that came from AWS. label Jun 18, 2020
@NetaNir NetaNir assigned NetaNir and unassigned eladb Jun 18, 2020
@NetaNir NetaNir added the pr/do-not-merge This PR should not be merged at this time. label Jun 19, 2020
Copy link
Contributor

@eladb eladb left a comment

Choose a reason for hiding this comment

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

Please add a unit tests

@NetaNir
Copy link
Contributor Author

NetaNir commented Jun 22, 2020

Please add a unit tests

@eladb Before I add a test, I just want to make sure we are good with the current behavior since the stage PR introduce a change in the way aspects are invoked.

Prior to the change, if AspectB was added via AspectA (meaning when AspectA visit is called), AspectB will be invoked on the node AspectA was applied to and all of its children. In the bellow code, both AspectA and AspectB will be invoked on the root and the child constructs.

class AspectB implements IAspect {
  public visit(construct: IConstruct): void {}
}

class AspectA implements IAspect {
  public visit(construct: IConstruct): void {
    construct.node.applyAspect(new AspectB())
  }
}

const app = new App();
const root = new Construct(app, 'MyConstruct');
root.node.applyAspect(new AspectA());
const child = new Construct(root, 'ChildConstruct');

After the change AspectB will not be invoked on the root or the child constructs.

@eladb
Copy link
Contributor

eladb commented Jun 22, 2020

I am okay with detecting this and issuing a warning.

// the `nestedAspectWarning` flag is used to prevent the warning from being emitted for every child
if (!nestedAspectWarning && nodeAspectsCount !== node._aspects.length) {
// tslint:disable-next-line: max-line-length
construct.node.addWarning('We detected an aspect was added via another aspect, this is not supported and may result in an unexpected behavior');
Copy link
Contributor

@rix0rrr rix0rrr Jun 22, 2020

Choose a reason for hiding this comment

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

We can be more specific than the vague "may result in unexpected behavior."

How about something like?

Aspects added by other Aspects will not be applied; add all Aspects before 'synth' is called.

Copy link
Contributor

@rix0rrr rix0rrr Jun 22, 2020

Choose a reason for hiding this comment

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

If we wanted to go with restoring the original behavior, it would look something like this:

const appliedHere = new Set<IAspect>();
let allAspectsHere: IAspect[];
while (true) {
  allAspectsHere = [...inheritedAspects ?? [], ...node._aspects];
  const yetToApply = allAspectsHere.filter(x => !appliedHere.has(x));
  if (yetToApply.length === 0) { break; }
  
  for (const aspect of yetToApply) { 
    // ...
    appliedHere.add(aspect);
  }
}

recurse(child, allAspectsHere);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@eladb @rix0rrr I think we should restore the previous behavior, if only to maintain backward compatibility, if no objection I'll create the PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

No objections

Copy link
Contributor

Choose a reason for hiding this comment

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

? Any updates?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

on it!

Copy link
Contributor

Choose a reason for hiding this comment

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

Any updates?

@NetaNir NetaNir requested a review from eladb July 28, 2020 17:53
@NetaNir NetaNir removed the pr/do-not-merge This PR should not be merged at this time. label Jul 30, 2020
@eladb eladb changed the title chore(core): warn if an aspect was added via another aspect feat(core): warn if an aspect was added via another aspect Aug 3, 2020
@eladb eladb added pr/ready-to-merge This PR is ready to be merged. pr-linter/exempt-readme The PR linter will not require README changes labels Aug 3, 2020
@mergify
Copy link
Contributor

mergify bot commented Aug 3, 2020

Thank you for contributing! Your pull request will be updated from master and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork).

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: 8e264a7
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@mergify
Copy link
Contributor

mergify bot commented Aug 3, 2020

Thank you for contributing! Your pull request will be updated from master and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork).

@mergify mergify bot merged commit 9d7bef7 into master Aug 3, 2020
@mergify mergify bot deleted the neta/warn-nested-aspects branch August 3, 2020 20:40
eladb pushed a commit that referenced this pull request Aug 10, 2020
### Updated PR

Since we introduced [`stages`](#8423) which had the unintended side effect of the CDK not supporting adding an Aspect via another Aspect, we have not seen any issues reported beside #8536, that was resolved without requiring this capability.
Given that, and the fact this features has many sharp edges we decided to leave it unsupported, and add a warning in the meantime. 

-----------

Following up on #8536

If an aspect is added via another aspect, the inner aspect  will  not be invoked. Take for example this code:

```typescript
const app = new cdk.App();

app.node.applyAspect({
  visit(construct: cdk.IConstruct) {
    construct.node.applyAspect({
      visit(construct: cdk.IConstruct) {
        console.info("Invoking aspect on construct: " + construct.node.id);  // This will not be called
      }
    })
  }
});
```

Since aspects are added only on the top level node,  if an aspect is added while `InvokeAspects` is called on that node,  it will be ignored since it will not be added to list of aspects to invoke (`allAspectsHere` in the bellow code):
```typescript
function invokeAspects(root: IConstruct) {
  recurse(root, []);

  function recurse(construct: IConstruct, inheritedAspects: constructs.IAspect[]) {
    // hackery to be able to access some private members with strong types (yack!)
    const node: NodeWithAspectPrivatesHangingOut = construct.node._actualNode as any;

    const allAspectsHere = [...inheritedAspects ?? [], ...node._aspects];

    for (const aspect of allAspectsHere) {
      if (node.invokedAspects.includes(aspect)) { continue; }

      aspect.visit(construct); <-- an aspect that was added here will not be added to `allAspectsHere` and will be ignored

      node.invokedAspects.push(aspect);
    }

    for (const child of construct.node.children) {
      if (!Stage.isStage(child)) {
        recurse(child, allAspectsHere);
      }
    }
  }
}
 
```

Assuming this is not something we want to support**, we can detect it by comparing the size of `node._aspects` before and after the call to `aspect.visit`, and emit a warning if there has been a change. Note that while the aspect will not be invoked it will be added to every child construct during the recursive visit. Emitting a warning for each child construct will result in a noisy console, to prevent this I have added a flag that will only allow adding **one warning per application**, given this limitation I'm not sure  there is a lot of value in adding the warning, thoughts?
 
If we decide to add it I will add tests.



 (** theoretically we could support it by adding the aspects to `allAspectsHere`  during the loop, but this will require a non trivial implementation in order to avoid infinite recursion) 



----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
curtiseppel pushed a commit to curtiseppel/aws-cdk that referenced this pull request Aug 11, 2020
### Updated PR

Since we introduced [`stages`](aws#8423) which had the unintended side effect of the CDK not supporting adding an Aspect via another Aspect, we have not seen any issues reported beside aws#8536, that was resolved without requiring this capability.
Given that, and the fact this features has many sharp edges we decided to leave it unsupported, and add a warning in the meantime. 

-----------

Following up on aws#8536

If an aspect is added via another aspect, the inner aspect  will  not be invoked. Take for example this code:

```typescript
const app = new cdk.App();

app.node.applyAspect({
  visit(construct: cdk.IConstruct) {
    construct.node.applyAspect({
      visit(construct: cdk.IConstruct) {
        console.info("Invoking aspect on construct: " + construct.node.id);  // This will not be called
      }
    })
  }
});
```

Since aspects are added only on the top level node,  if an aspect is added while `InvokeAspects` is called on that node,  it will be ignored since it will not be added to list of aspects to invoke (`allAspectsHere` in the bellow code):
```typescript
function invokeAspects(root: IConstruct) {
  recurse(root, []);

  function recurse(construct: IConstruct, inheritedAspects: constructs.IAspect[]) {
    // hackery to be able to access some private members with strong types (yack!)
    const node: NodeWithAspectPrivatesHangingOut = construct.node._actualNode as any;

    const allAspectsHere = [...inheritedAspects ?? [], ...node._aspects];

    for (const aspect of allAspectsHere) {
      if (node.invokedAspects.includes(aspect)) { continue; }

      aspect.visit(construct); <-- an aspect that was added here will not be added to `allAspectsHere` and will be ignored

      node.invokedAspects.push(aspect);
    }

    for (const child of construct.node.children) {
      if (!Stage.isStage(child)) {
        recurse(child, allAspectsHere);
      }
    }
  }
}
 
```

Assuming this is not something we want to support**, we can detect it by comparing the size of `node._aspects` before and after the call to `aspect.visit`, and emit a warning if there has been a change. Note that while the aspect will not be invoked it will be added to every child construct during the recursive visit. Emitting a warning for each child construct will result in a noisy console, to prevent this I have added a flag that will only allow adding **one warning per application**, given this limitation I'm not sure  there is a lot of value in adding the warning, thoughts?
 
If we decide to add it I will add tests.



 (** theoretically we could support it by adding the aspects to `allAspectsHere`  during the loop, but this will require a non trivial implementation in order to avoid infinite recursion) 



----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/core Related to core CDK functionality contribution/core This is a PR that came from AWS. pr/ready-to-merge This PR is ready to be merged. pr-linter/exempt-readme The PR linter will not require README changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants