fix(memorystore): prevent infinite reconciliation drift loop by aligning connections list length#11547
Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
0a6c617 to
4306a49
Compare
| desired.CrossInstanceReplicationConfig = maskedActual.CrossInstanceReplicationConfig | ||
| } | ||
|
|
||
| // Align connections list length to prevent false drift detection on server-generated connections |
There was a problem hiding this comment.
Not a blocker of this PR.
This seems another use case for the issue I've experienced: The API converts the desired state into a value that we cannot easily simulate on our end, unlike predictable conversions we already handle(e.g., mapping project ID to number or altering string/enum casing, ordering, etc).
I am leaning toward a more generic solution that deserves a design discussion: storing a hash(last applied desired state, last applied actual state) in new field "ObservedState.LastApplied". We can then compare the current hash(desired state, actual state) against this stored hash to detect diffs. We should document the specific fields requiring this logic so we don't hash all fields unnecessarily.
There was a problem hiding this comment.
Yeah this makes sense, a nice thing in this case is that we do store the received values in status so we shouldn't lose this data
There was a problem hiding this comment.
I think this is a different use case? The reason we want observedState to store the actual value for comparison is that the service-returned value and the user-provided value are different. And for this use case, the service still return the same values.
edbbffd to
b452612
Compare
| for _, actualEndpoint := range actualEndpoints { | ||
| for _, conn := range actualEndpoint.GetConnections() { | ||
| if k := connectionKey(conn); k != "" { | ||
| actualEndpointByConnection[k] = actualEndpoint |
There was a problem hiding this comment.
Nit: Looping through connections made me think different connections will generate different keys. Might be more readable if the loop is hidden by a function name.
| func isConnectionSubset(desiredConnections, actualConnections []*memorystorepb.Instance_ConnectionDetail) bool { | ||
| actualSet := make(map[string]bool, len(actualConnections)) | ||
| for _, conn := range actualConnections { | ||
| if k := connectionKey(conn); k != "" { |
There was a problem hiding this comment.
I think all connections in a single endpoint group belong to the same VPC network, and they all have the same network key. So this loop ends up writing the exact same key over and over again. actualSet will contain exactly one unique key (e.g., {"my-project/my-vpc": true}). Right?
| continue | ||
| } | ||
|
|
||
| if _, found := actualSet[k]; !found { |
There was a problem hiding this comment.
And we already matched the parent desiredEndpoint and actualEndpoint using this network key k in the caller function (alignEndpointsWithDesired), we already know that "my-project/my-vpc" is the key for both. Thus, this lookup will always succeed. Right?
I guess my question is how we identify the desired connections is a subset of the actual connections here.
There was a problem hiding this comment.
I think the isConnectionSubset check is not necessary.
There was a problem hiding this comment.
This will check that all connections in desired are present in actual connections. If a desired connection is not present it can't be a subset.
The top level matching where we use the connection as keys is mostly an artifact of endpoints only having the connections field and thus not having a better field to key by and although it may end up guaranteeing some correctness it's best not to depend on the logic.
The order here should be:
- Match endpoints what alignEndpointsWithDesired is trying to do (in a poor way due to keying limitation)
- Check if desired endpoints connections are a subset of actual
There was a problem hiding this comment.
Thanks for the explanation!
One minor observation on the updated connectionsMatch function:
Since the KRM schema for PscAutoConnection only defines projectRef and networkRef in the Spec, output-only fields like PscConnectionId, IpAddress, ForwardingRule, ServiceAttachment, and Port will never be populated on the desired connection object.
As a result, the checks for those output-only fields in connectionsMatch will always be skipped (evaluating to true). In practice, the matching is based solely on ProjectId and Network.
That's why I suggested we remove the isConnectionSubset function previously.
To make the code cleaner and prevent future confusion about what is actually being compared, I suggest we simplify connectionsMatch to only check the fields that can actually be set in the KRM spec (i.e., ProjectId and Network / projectRef and networkRef). What do you think?
422f3dc to
c73c750
Compare
3a91da7 to
6711204
Compare
…s with injective subset connection matching
6711204 to
745bc77
Compare
Why is this change necessary?
During E2E testing of network connectivity topologies, we observed that
MemorystoreInstancecontrollers can fall into an infinite reconciliation loop due to false drift detection on theendpoints[i].connectionslist.Specifically:
desired), a user defines the target VPC network(s) underspec.endpoints[i].connections(typically 1 connection item representing the intended PSC network target).actualconnections list that is longer than the user'sdesiredlist,tags.DiffForTopLevelFieldsflags the length mismatch as a drift. KCC repeatedly sends update requests attempting to shrink the connections list, which GCP ignores, resulting in an infinite reconciliation loop.What does this change do?
Connectionslist length before diffing (pkg/controller/direct/memorystore/memorystoreinstance_controller.go):Truncates the server-generated extra
ConnectionsinmaskedActualto matchlen(desiredEndpoint.Connections)before comparison (actualEndpoint.Connections = actualEndpoint.Connections[:len(desiredEndpoint.Connections)]).maskedActualcopy used for drift comparison against the user'sspec. It does not discard any user-configured target connections, nor does it affect the full observed state reported instatus.endpoints. It ensures we compare exactly the user-declared intent (desired) against the corresponding actual connection items without failing on extra server-created replica entries.pkg/controller/direct/directbase/operations.go):Updates
directbase/operations.goto prevent partial status updates from overwriting existing status fields (such asconditionsorexternalRef) withnil.Aligns/sorts HTTP log entries for the
MemorystoreInstancetest fixtures to prevent test flakiness.