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

Refactor detailed diff v2 to short circuit on nulls and unknowns #2496

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

VenelinMartinov
Copy link
Contributor

@VenelinMartinov VenelinMartinov commented Oct 17, 2024

This PR changes the logic in the detailed diff v2 calculation to short-circuit on encountering nils and unknowns.

Previously, when we encountered a null or unknown in either news or olds we would still recurse down and compare the non-nil values versus a nil value in order to get any replacements which might be happening. We would then simplify the diff later to trim these extra diffs but would propagate the replacement to the right level.

We would now instead short-circuit on encountering a null or unknown and walk the non-null value to find any properties which can trigger a replacement. This makes it much easier to handle sets in #2451 as recursing into sets is not necessary, as they are only compared by hashes.

This change is not meant to change any behaviour and is tested in #2405

Copy link

codecov bot commented Oct 17, 2024

Codecov Report

Attention: Patch coverage is 88.23529% with 12 lines in your changes missing coverage. Please review.

Project coverage is 62.60%. Comparing base (0e0d2c4) to head (e13ae47).
Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
pkg/tfbridge/detailed_diff.go 88.23% 8 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2496      +/-   ##
==========================================
+ Coverage   62.58%   62.60%   +0.01%     
==========================================
  Files         380      380              
  Lines       51265    51294      +29     
==========================================
+ Hits        32086    32112      +26     
- Misses      17363    17365       +2     
- Partials     1816     1817       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

if !isPresent(val) {
return false
}
switch propType {
Copy link
Member

Choose a reason for hiding this comment

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

The logic in isTypeShapeMismatched only works when val doesn't have any marker types, so lets assert that.

Suggested change
switch propType {
contract.Assertf(!val.IsComputed() && !val.IsSecret() && !val.IsOutput(),
"Unexpected marked value %#v", val)
switch propType {

if !visitor(path, val) {
return false
}

Copy link
Member

Choose a reason for hiding this comment

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

Again, I'd like to see an assert that we are not hitting marker types.

Copy link
Member

Choose a reason for hiding this comment

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

Just was commenting on another PR, but this is not a complete walker on PropertyValue.

E.G.

https://github.com/pulumi/pulumi-terraform-bridge/blob/master/unstable/propertyvalue/propertyvalue.go#L42

If we had a complete walker on property value we would not need to double-test it. I know Go is about "a bit more duplication is better than a bit more sharing", but if we can reuse that makes coverage goals easier to achieve.

return true
}

func willTriggerReplacement(
Copy link
Member

Choose a reason for hiding this comment

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

Can you comment on why we need willTriggerReplacement and willTriggerReplacementRecursive.

Copy link
Member

Choose a reason for hiding this comment

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

I think this code decides presentation. We already decided if the plan is replace or not. This code decides which change to blame for replacing so it's easier to see visually why a replace is happening. Is that correct VM?

return isForceNew(tfs, ps)
}

func willTriggerReplacementRecursive(
Copy link
Member

Choose a reason for hiding this comment

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

The name for this function needs to describe what it does, not how its' implemented.

Comment on lines +120 to +122
if tfs.Type() != shim.TypeList && tfs.Type() != shim.TypeSet && tfs.Type() != shim.TypeMap {
return false
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm surprised that this is (1) only effected by one level up and (2) doesn't include ForceNew on objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I understand these - can you please expand on this?

Copy link
Member

Choose a reason for hiding this comment

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

I'm surprised that we don't search all the way up the schema tree, not just one level up.

I think I was wrong about not including objects. tfs.Type() doesn't express objects.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I was wondering this too. We can probably get a away with something approximate as it's presentation-layer only but worth backlogging at least.

Comment on lines +386 to +387
oldMap := old.ObjectValue()
newMap := new.ObjectValue()
Copy link
Member

Choose a reason for hiding this comment

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

Just so I understand, this is ok because of isTypeShapeMismatched?

@iwahbe
Copy link
Member

iwahbe commented Oct 18, 2024

Reviewing the tests added in #2405, they look sufficient for nested forcenew in top level and singly nested cases. Can you point me to where multiple nesting levels are tested? I want to see a test that validates our behavior on a schema like this:

List{
  ForceNew: true,
  Elem: Object{
    Fields: {
       "f0": String,
    }
  },
}

I couldn't find tests for more then one level of nesting.

func willTriggerReplacement(
path propertyPath, rootTFSchema shim.SchemaMap, rootPulumiSchema map[string]*info.Schema,
) bool {
// A change on a property might trigger a replacement if:
Copy link
Member

Choose a reason for hiding this comment

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

The function seems to do what the comment says. The rule sounds a little un-intuitive but looks like you've explored this thoroughly in tests so perhaps that's that?

If a property P is forcenew, but P.a.b.c.d has changed, I am guessing that the overall plan is a replace plan, is that right? If we are displaying the detailed diff as a.b.c.d has changed we are probably within rights to blame d for replacing?

@@ -264,13 +294,48 @@ func (differ detailedDiffer) makePlainPropDiff(
return nil
}

// makeShortCircuitDiff is used for properties that are nil or computed in either the old or new state.
Copy link
Member

Choose a reason for hiding this comment

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

This is .. tricky.. If a property is Computed or unknown, we do not know if it will trigger a replacement or not, until we know it. It might trigger a replacement, or it might not. Any implementation here takes a guess and we need to probably call this out in a comment that we are guessing. I would look up current Pulumi behavior for this case and copy how we're guessing to continue in the tradition we have.

differ := detailedDiffer{
tfs: shimv2.NewSchemaMap(sdkv2Schema),
}
tfs := shimv2.NewSchemaMap(sdkv2Schema)
Copy link
Member

Choose a reason for hiding this comment

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

The changes to tests look very unconvincing 🙏

You're going for no change in behavior in this PR?

Can you point me to a test case that illustrates this "short-circuit" diff so I can build up some intuition? Perhaps this all is under tests already. Thank you!

Do not want to block on this but would like to understand better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants