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

hashring.LookupN returns ordered results #211

Merged
merged 6 commits into from
Jul 26, 2017

Conversation

andrewdeandrade
Copy link
Contributor

@andrewdeandrade andrewdeandrade commented Jul 25, 2017

The previous implementation of LookupN used a map internally to guarantee uniqueness, but when converted to a slice, ordering of servers in the hashring is lost. This diff updates the internal implementation to maintain a pointer to a slice that is appended during the depth-first search of the red-black tree. This ordered result is converted to a slice of strings to return to the caller.

cc/ @yulunli @akshayjshah

@andrewdeandrade
Copy link
Contributor Author

FWIW, I'm adding this so I can then add ordered LookupN to the router. My service is stateful and push based, so on graceful shutdown I have an interest in iterating through the N servers in the ring. If the forward fails to the first server, go to the next. If that fails, go to the next, etc.

Copy link
Contributor

@akshayjshah akshayjshah left a comment

Choose a reason for hiding this comment

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

Goal seems totally reasonable to me, but I'm a ringpop amateur.


// lookup N unique servers from the red-black tree. If we have not
// collected all the servers we want, we have reached the
// end of the red-black tree and we need to loop around and inspect the
// tree starting at 0.
r.tree.LookupNUniqueAt(n, replicaPoint{hash: hash}, unique)
r.tree.LookupNUniqueAt(n, replicaPoint{hash: hash}, unique, &orderedUnique)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an unusual way to work with slices - it's typical to return the modified slice instead of passing pointers. Why pass a ref here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The append in findNUniqueAbove replaces the previous slice with a new one at a different address. Is there a way I could accomplish this without using pointers to the slice and updating the pointer?

Copy link
Contributor

Choose a reason for hiding this comment

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

You could model this API after append and return the modified slice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure I understand what you mean here. I somewhat suspect that what you're suggesting is what I ended up doing in my test code.

@@ -310,7 +310,8 @@ func TestLookupNLoopAround(t *testing.T) {
ring.AddMembers(members...)

unique := make(map[valuetype]struct{})
ring.tree.LookupNUniqueAt(1, replicaPoint{hash: 0}, unique)
orderedUnique := make([]valuetype, 0, 1)
ring.tree.LookupNUniqueAt(1, replicaPoint{hash: 0}, unique, &orderedUnique)
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't test the correctness of the code you've added; please add unit tests to cover the new logic.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd love a test for correctness here as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any suggestions on how to test this given the way the tests are written so far? Naively, I could just iterate a few times and confirm that the returned results for multiple interations of the same call are stable (i.e. same order), but that still wouldn't test that the order is what we expect.

Copy link
Contributor

@akshayjshah akshayjshah Jul 25, 2017

Choose a reason for hiding this comment

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

If I understand this change correctly (debatable), you should be able to assert that (1) the returned slice has the correct length, (2) that the sort is stable, and (3) that the first item is the same as the returned value from Lookup.

@thanodnl may have some ideas for more thorough testing; the only further tests I can imagine would require using the farmhash code directly.

Copy link
Contributor Author

@andrewdeandrade andrewdeandrade Jul 25, 2017

Choose a reason for hiding this comment

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

I just added tests. See what you all think. I did have to remove the optimization that copied the servers because it copied them out of order because it copies from a map. I also considered a straight traversal copy, but that would make the first server in the tree hot.

Essentially, my tests implement the same feature but using the built-in traverseWhile, so the tests and implementation are the same "feature" checking each other.

Copy link
Contributor

@thanodnl thanodnl left a comment

Choose a reason for hiding this comment

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

Thanks for this fix! I quickly checked how ringpop-node works and there the list is indeed ordered.

There are two things that I'd like to see fixed marked below. I have no strong opinion about slice vs slicepointer but I have to agree it is not something you see often in golang. Hopefully you two can work together in SF time.

When reach consensus don't wait for me to wake up before landing. Functionally it is completely fine with me.

@@ -399,7 +400,7 @@ func (s *ProcessMembershipChangesSuite) TestAddMember0() {
s.ring.ProcessMembershipChanges([]membership.MemberChange{
{After: s.members[0]},
})
mock.AssertExpectationsForObjects(s.T(), s.l.Mock)
mock.AssertExpectationsForObjects(s.T(), &s.l.Mock)
Copy link
Contributor

Choose a reason for hiding this comment

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

This line seems to makes the tests fail with a panic.

@@ -310,7 +310,8 @@ func TestLookupNLoopAround(t *testing.T) {
ring.AddMembers(members...)

unique := make(map[valuetype]struct{})
ring.tree.LookupNUniqueAt(1, replicaPoint{hash: 0}, unique)
orderedUnique := make([]valuetype, 0, 1)
ring.tree.LookupNUniqueAt(1, replicaPoint{hash: 0}, unique, &orderedUnique)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd love a test for correctness here as well.

@andrewdeandrade
Copy link
Contributor Author

Unless there are no more objections, I'm going to land this in an hour. As far as I can tell all issues raised so far have been addressed.

@andrewdeandrade andrewdeandrade merged commit a73855f into master Jul 26, 2017
@andrewdeandrade andrewdeandrade deleted the aandrade.ordered-hashring-lookup-n branch July 27, 2017 19:48
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