-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Only read relevant nodes from database in PeerChangedResponse #2509
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
kradalby
left a comment
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 think the general change makes sense, in general a change request is (in my observation) not more than a subset of nodes, like you call out.
I am up for this, but it needs to be fetched as ones.
| ) ([]byte, error) { | ||
| resp := m.baseMapResponse() | ||
|
|
||
| peers, err := m.ListPeers(node.ID) |
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 agree this is not necessary, but instead of listing all, we should just list the one from the change map.
It would make more sense to make ListPeers(node NodeID, peerIDs ...NodeID) and only fetch the ones that are needed here.
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 opted against changing ListPeers for now. It is also used for full map responses, where I think all peers are needed (although I have not looked too deeply into this function yet). But I can of course change the naming scheme if you like something like ListPeersSubset better than ListNodesSubset.
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 think modifying both ListNodes and ListPeers to be able to take a optional list arg would be the best.
hscontrol/mapper/mapper.go
Outdated
| changedNodes = append(changedNodes, peer) | ||
| for _, changedID := range changedIDs { | ||
| if changedID != node.ID { | ||
| changedNode, err := m.GetNode(changedID) |
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 problem with doing it this way, and not as mentioned above is that we dont get transactional consistency. In this case, nodes might change in between calls to GetNode. While this might not be an obvious problem, it might lead to incredibly hard to debug problems.
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 have reworked the code to use exactly one database query. I will also add a test to the database query function at least.
|
The code is reworked to use exactly one database query and I also added a unit test. I have some concern that my unit test does not cover the case of a NodeID being zero, although the code to add nodes in the test seem to suggest that this case cannot happen anyway. |
kradalby
left a comment
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.
Much better, couple of comments to simplify and keep around lest functions/code.
CHANGELOG.md
Outdated
| [#2493](https://github.com/juanfont/headscale/pull/2493) | ||
| - If a OIDC provider doesn't include the `email_verified` claim in its ID | ||
| tokens, Headscale will attempt to get it from the UserInfo endpoint. | ||
| - Improve performance by only querying relevant nodes from the database for node updates |
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.
please link the pr
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
hscontrol/db/node.go
Outdated
| return nodes, nil | ||
| } | ||
|
|
||
| func (hsdb *HSDatabase) ListNodesSubset(nodeIDs types.NodeIDs) (types.Nodes, error) { |
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.
| func (hsdb *HSDatabase) ListNodesSubset(nodeIDs types.NodeIDs) (types.Nodes, error) { | |
| func (hsdb *HSDatabase) ListNodes(nodeIDs ...types.NodeID) (types.Nodes, error) { |
I would prefer to just have the same function for this, but use the list expansion argument like this so it remains optional for getting all.
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.
Is implemented.
| ) ([]byte, error) { | ||
| resp := m.baseMapResponse() | ||
|
|
||
| peers, err := m.ListPeers(node.ID) |
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 think modifying both ListNodes and ListPeers to be able to take a optional list arg would be the best.
hscontrol/mapper/mapper.go
Outdated
| return peers, nil | ||
| } | ||
|
|
||
| func (m *Mapper) ListNodes(nodeIDs ...types.NodeIDs) (types.Nodes, error) { |
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.
You need to pass it as types.NodeID (without the s), now you are passing it as a list of lists
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.
That is by design. When using an optional argument we face the problem of having two different types of empty lists, both of which are needed:
- The empty list of not using the optional argument. We want to return all nodes in this case.
- The empty list of using the optional argument, but with a list where all nodes have been filtered out. We want to return an empty list here and not all nodes.
One could use the nil and []type.NodeID{} variants of empty lists to distinguish these two cases. But in my experience, code depending on different behavior between these two empty list variants is a future bug waiting to happen. I know that my design also has a hidden footgun in that one could pass more than one list of nodes as parameter to the function. But I would prefer having two ListNodes functions before changing the parameter type to ...types.NodeID.
Side remark: I will wait until we have settled on a design for ListNodes before I take a second look at ListPeers.
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 empty list of using the optional argument, but with a list where all nodes have been filtered out. We want to return an empty list here and not all nodes.
This scenario does not make any sense to me, in the case where all nodes has been filtered out, you would have an empty list of IDs and you would instead not call for the database and return early.
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.
Well, the proposed code does check it and returns early right before the database access would otherwise occur. In my opinion it is better than relying on the caller to know that he has to check it before calling ListNodes.
As context: In my load tests this case did actually happen and there was no check in PeerChangedResponse before querying the database for zero 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.
I think it will be fine with a docstring explaining the filter. Current behaviour is not that ergonomic and I would want it to be.
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.
Although I do not like it from a coding-style perspective, I implemented it now.
Pull Request Revisions
|
|
Ok, |
|
Looks great, thank you! |
|
I added the same functionality to ListPeers (currently not used anywhere). |
kradalby
left a comment
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.
Great! thank you!
Querying the whole database becomes a performance issue if a lot of nodes exist in it, even if all those nodes are offline. In
PeerChangedResponsewe actually know which nodes need to be queried, so we can restrict the database queries to these nodes.In my tests,
PeerChangedResponseusually needed to query less than 5 nodes, so the change resulted in an order of magnitude better performance for a database with several hundred nodes (most of them offline).I can also write a test if desired. The PR should not change any behavior, so I wanted to base tests on existing tests for
PeerChangedResponse. However, I did not find any existing tests for this function, so I am happy for pointers where to start for testingPeerChangedResponse.