-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
resolver: Add an endpoint map type #6679
Conversation
resolver/map.go
Outdated
// Equal returns whether the unordered set of addrs counts are the same between | ||
// the endpoint nodes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use the counts? I don't think an endpoint should include the same address multiple times, and if it does, it seems like we'd just ignore the duplicates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I use the counts because it feels like a defensive way with no cost incured. I.e. I either do a presence check with a struct{} or bool or I have the value be an int. I know it's not an intended use case of an endpoint to have the same address (by string) multiple times but I added this since it seems to be similar to add it vs. presence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more about how we want it to behave vs. what's an easy feature to add in.
In general I think we want to consider these endpoints as equivalent, so we should be using a set of addresses instead of a set with counts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping. Let's make this a set, which is how the docstring below indicates it behaves as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still technically a set, right? I don't see any downside to doing it this way, and I think it is more technically correct to take into account address counts (still unordered). Technically, what do you see as the pro of AABC == ABC vs AABC != ABC? I feel like it makes more sense that they're not equivalent (and this case will never happen in practice anyway). It allows the resolver to encode more information in the endpoint (250 ms delay fail address a, then try address a again)...hmmm. I don't think happy eyeballs makes sense wrt doing aa, but right now unless we use keys() to pass down to child the duplicated addresses in an endpoint will be conveyed to the pick first child. So I think whether we account for address counts here is orthogonal to whether we want to pass down AABC to pick first child and have it do happy eyeballs algorithm on AA (relates to that discussion we had monday about pick first never reporting TF).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I put this in the dualstack design chat. Will wait for more discussion before choosing to implement this suggestion or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mark said to strip counts, so will implement this suggestion.
resolver/map.go
Outdated
// unordered set of addresses string within an endpoint. The reason this uses a | ||
// slice instead of a map is because slices or maps cannot be used as map keys. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
External documentation should not include implementation details or rational for implementation details unless it's important to the user of the type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, ok noted. Moved the comment to the field (which is unexported and users of this package won't see it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also added a top level comment to external type about it not being thread safe and cannot access multiple times concurrently.
resolver/map.go
Outdated
// NewEndpointMap returns a new EndpointMap. | ||
func NewEndpointMap() *EndpointMap { | ||
return &EndpointMap{} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally if the zero value of the struct is safe to use, we'd just avoid having a constructor and say the zero value is safe to use as an empty map. E.g. https://pkg.go.dev/bytes@go1.21.1#Buffer
The exception is if you think it might need an initialization later, especially if it's an API that we can't ever change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah since you can append and operate on nil slices the zero value here should be safe to use. Deleted this constructor and use the zero value in tests. However if I go down the map way you mentioned in another comment I'll need to construct that map, since nil doesn't work for map operations (would panic). If I go down that route I'll keep it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per comment below, chose not to go map route so deleted this constructor. If we do need to change this, it's internal and we won't break users :).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not internal. This is in the grpc/resolver
package, which is experimental and intended to be accessed by users, but subject to change. But breaking changes will still break users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, ok sounds good.
resolver/map.go
Outdated
if count, ok := en[addr.Addr]; ok { | ||
en[addr.Addr] = count + 1 | ||
} | ||
en[addr.Addr] = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
en[addr.Addr]++
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, switched.
resolver/map.go
Outdated
en := toEndpointNode(e) | ||
entry := em.endpoints.find(en) | ||
if entry == -1 { | ||
return | ||
} | ||
if len(em.endpoints) == 1 { | ||
em.endpoints = nil | ||
} | ||
copy(em.endpoints[entry:], em.endpoints[entry+1:]) | ||
em.endpoints = em.endpoints[:len(em.endpoints)-1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option would be to make endpoints
a map[*endpointNode]struct{}
. find
would return the *endpointNode
if the endpoint can be found. Delete
can be a simple delete(endpoints, entry)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue I see with using a map of unordered addresses in a map itself is it's not allowed (similar to how you exclude attributes in your address map, since map[key] underlying using something similar to cmp.Equal, which panics on a map key, as you mentioned offline). Thus, I made it a list, with o(n) operations not on the RPC path, so I can do comparisons on each node. I don't think this would work with a map, unless you can overwrite the _, ok := map[key] operation, which I have not have found a way to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you weren't understanding my suggestion. Pointers are valid for map keys, and are compared using pointer equality:
https://go.dev/play/p/VWhAQUEueKR
We wouldn't do lookups using the key, though, even though a map was used. But this simplifies deletions to: 1. find the entry to be deleted by scanning all the entries; 2. delete(endpoints, entryPtr)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. Switched to that.
Codecov Report
Additional details and impacted files |
resolver/map.go
Outdated
// unordered set of address strings within an endpoint. This map is not thread | ||
// safe, cannot access multiple times concurrently. The zero value for an |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"This map is not thread safe, cannot access multiple times concurrently."
Please fix the grammar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched to "This map is not thread safe, thus it is unsafe to access concurrently".
resolver/map.go
Outdated
// safe, cannot access multiple times concurrently. The zero value for an | ||
// EndpointMap is an empty EndpointMap ready to use. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The zero value is no longer safe to use, so change comment to say to use NewEndpointMap to create.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah whoops yeah forgot to change this. Switched comment to what you said for address map switched to endpoint map: "Must be created via NewEndpointMap; do not construct directly."
resolver/map.go
Outdated
// Equal returns whether the unordered set of addrs counts are the same between | ||
// the endpoint nodes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping. Let's make this a set, which is how the docstring below indicates it behaves as well.
resolver/map.go
Outdated
// addresses present in the endpoint keys, in which uniqueness is determined by | ||
// the unordered set of addresses. Thus, endpoint information returned is not | ||
// the full endpoint data but can be used for EndpointMap accesses. | ||
func (em *EndpointMap) Keys() []Endpoint { // TODO: Persist the whole endpoint data to return in nodes? No use case now, but one could come up in future. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure we don't already need it? You can't create/update a child policy without the full endpoint data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. This usage of Keys() for resolver map is simply a helper to index back into the map:
grpc-go/examples/features/customloadbalancer/client/customroundrobin/customroundrobin.go
Line 139 in b0e5540
for _, e := range crr.pfs.Keys() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deleted this TODO and added information to top level docstring.
resolver/map.go
Outdated
// find returns the pointer to endpoint node in the EndpointMap endpoints map if | ||
// the endpoint node is already present. If not found, nil is returned. The |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
find returns a pointer to the endpoint node
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I don't get what is wrong with this statement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The wrong part is "to endpoint node". You forgot article. Also we usually talk about pointers as indefinite ("a pointer to") vs. definite ("the pointer to") since there can be more than one pointer to a thing (non Highlander), so my suggestion updated that as well.
While you're here, "in the EndpointMap endpoints map" stutters; you really only need one of those. Or "in em" would be even shorter and more precise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
resolver/map.go
Outdated
entry := em.find(en) | ||
if entry != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merge lines and reduce variable scope: if entry := em.find(en); entry != nil {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright sounds good. Done.
This PR adds an endpoint map type for use by Petiole policies for pick first children and also balancers like Outlier Detection which store state unique to individual endpoints. The endpoints are unique based off the unordered set of address string counts within an endpoint.
RELEASE NOTES: