Repository: serialx/hashring Branch: master Commit: 22c0c7ab6b1b Files: 10 Total size: 26.8 KB Directory structure: gitextract_o_woei36/ ├── .gitignore ├── LICENSE ├── README.md ├── allnodes_test.go ├── benchmark_test.go ├── example_test.go ├── hash.go ├── hashring.go ├── hashring_test.go └── key.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ coverage *.html ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Sung-jin Hong Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ hashring ============================ Implements consistent hashing that can be used when the number of server nodes can increase or decrease (like in memcached). The hashing ring is built using the same algorithm as libketama. This is a port of Python hash_ring library in Go with the extra methods to add and remove nodes. Using ============================ Importing :: ```go import "github.com/serialx/hashring" ``` Basic example usage :: ```go memcacheServers := []string{"192.168.0.246:11212", "192.168.0.247:11212", "192.168.0.249:11212"} ring := hashring.New(memcacheServers) server, _ := ring.GetNode("my_key") ``` To fulfill replication requirements, you can also get a list of servers that should store your key. ```go serversInRing := []string{"192.168.0.246:11212", "192.168.0.247:11212", "192.168.0.248:11212", "192.168.0.249:11212", "192.168.0.250:11212", "192.168.0.251:11212", "192.168.0.252:11212"} replicaCount := 3 ring := hashring.New(serversInRing) server, _ := ring.GetNodes("my_key", replicaCount) ``` Using weights example :: ```go weights := make(map[string]int) weights["192.168.0.246:11212"] = 1 weights["192.168.0.247:11212"] = 2 weights["192.168.0.249:11212"] = 1 ring := hashring.NewWithWeights(weights) server, _ := ring.GetNode("my_key") ``` Adding and removing nodes example :: ```go memcacheServers := []string{"192.168.0.246:11212", "192.168.0.247:11212", "192.168.0.249:11212"} ring := hashring.New(memcacheServers) ring = ring.RemoveNode("192.168.0.246:11212") ring = ring.AddNode("192.168.0.250:11212") server, _ := ring.GetNode("my_key") ``` ================================================ FILE: allnodes_test.go ================================================ package hashring import ( "fmt" "sort" "strconv" "testing" "github.com/stretchr/testify/assert" ) func generateWeights(n int) map[string]int { result := make(map[string]int) for i := 0; i < n; i++ { result[fmt.Sprintf("%03d", i)] = i + 1 } return result } func generateNodes(n int) []string { result := make([]string, 0, n) for i := 0; i < n; i++ { result = append(result, fmt.Sprintf("%03d", i)) } return result } func TestListOf1000Nodes(t *testing.T) { testData := map[string]struct { ring *HashRing }{ "nodes": {ring: New(generateNodes(1000))}, "weights": {ring: NewWithWeights(generateWeights(1000))}, } for testName, data := range testData { ring := data.ring t.Run(testName, func(t *testing.T) { nodes, ok := ring.GetNodes("key", ring.Size()) assert.True(t, ok) if !assert.Equal(t, ring.Size(), len(nodes)) { // print debug info on failure sort.Strings(nodes) fmt.Printf("%v\n", nodes) return } // assert that each node shows up exatly once sort.Strings(nodes) for i, node := range nodes { actual, err := strconv.ParseInt(node, 10, 64) if !assert.NoError(t, err) { return } if !assert.Equal(t, int64(i), actual) { return } } }) } } ================================================ FILE: benchmark_test.go ================================================ package hashring import "testing" func BenchmarkNew(b *testing.B) { nodes := []string{"a", "b", "c", "d", "e", "f", "g"} b.ResetTimer() for i := 0; i < b.N; i++ { _ = New(nodes) } } func BenchmarkHashes(b *testing.B) { nodes := []string{"a", "b", "c", "d", "e", "f", "g"} ring := New(nodes) tt := []struct { key string nodes []string }{ {"test", []string{"a", "b"}}, {"test", []string{"a", "b"}}, {"test1", []string{"b", "d"}}, {"test2", []string{"f", "b"}}, {"test3", []string{"f", "c"}}, {"test4", []string{"c", "b"}}, {"test5", []string{"f", "a"}}, {"aaaa", []string{"b", "a"}}, {"bbbb", []string{"f", "a"}}, } b.ResetTimer() for i := 0; i < b.N; i++ { o := tt[i%len(tt)] ring.GetNodes(o.key, 2) } } func BenchmarkHashesSingle(b *testing.B) { nodes := []string{"a", "b", "c", "d", "e", "f", "g"} ring := New(nodes) tt := []struct { key string nodes []string }{ {"test", []string{"a", "b"}}, {"test", []string{"a", "b"}}, {"test1", []string{"b", "d"}}, {"test2", []string{"f", "b"}}, {"test3", []string{"f", "c"}}, {"test4", []string{"c", "b"}}, {"test5", []string{"f", "a"}}, {"aaaa", []string{"b", "a"}}, {"bbbb", []string{"f", "a"}}, } b.ResetTimer() for i := 0; i < b.N; i++ { o := tt[i%len(tt)] ring.GetNode(o.key) } } ================================================ FILE: example_test.go ================================================ package hashring import ( "crypto/sha1" "fmt" ) func ExampleGetAllNodes() { hashRing := New([]string{"node1", "node2", "node3"}) nodes, _ := hashRing.GetNodes("key", hashRing.Size()) fmt.Printf("%v", nodes) // Output: [node3 node2 node1] } func ExampleCustomHashError() { _, err := NewHash(sha1.New).Use(NewInt64PairHashKey) fmt.Printf("%s", err.Error()) // Output: can't use given hash.Hash with given hashKeyFunc: expected 16 bytes, got 20 bytes } func ExampleCustomHash() { hashFunc, _ := NewHash(sha1.New).FirstBytes(16).Use(NewInt64PairHashKey) hashRing := NewWithHash([]string{"node1", "node2", "node3"}, hashFunc) nodes, _ := hashRing.GetNodes("key", hashRing.Size()) fmt.Printf("%v", nodes) // Output: [node2 node1 node3] } func ExampleNewHashFunc() { hashFunc, _ := NewHash(sha1.New).FirstBytes(16).Use(NewInt64PairHashKey) fmt.Printf("%v\n", hashFunc([]byte("test"))) // Output: &{-6441359348440544599 -8653224871661646820} } ================================================ FILE: hash.go ================================================ package hashring import ( "fmt" "hash" ) // HashSum allows to use a builder pattern to create different HashFunc objects. // See examples for details. type HashSum struct { functions []func([]byte) []byte } func (r *HashSum) Use( hashKeyFunc func(bytes []byte) (HashKey, error), ) (HashFunc, error) { // build final hash function composed := func(bytes []byte) []byte { for _, f := range r.functions { bytes = f(bytes) } return bytes } // check function composition for errors testResult := composed([]byte("test")) _, err := hashKeyFunc(testResult) if err != nil { const msg = "can't use given hash.Hash with given hashKeyFunc" return nil, fmt.Errorf("%s: %w", msg, err) } // build HashFunc return func(key []byte) HashKey { bytes := composed(key) hashKey, err := hashKeyFunc(bytes) if err != nil { // panic because we already checked HashSum earlier panic(fmt.Sprintf("hashKeyFunc failure: %v", err)) } return hashKey }, nil } // NewHash creates a new *HashSum object which can be used to create HashFunc. // HashFunc object is thread safe if the hasher argument produces a new hash.Hash // each time. The produced hash.Hash is allowed to be non thread-safe. func NewHash(hasher func() hash.Hash) *HashSum { return &HashSum{ functions: []func(key []byte) []byte{ func(key []byte) []byte { hash := hasher() hash.Write(key) return hash.Sum(nil) }, }, } } func (r *HashSum) FirstBytes(n int) *HashSum { r.functions = append(r.functions, func(bytes []byte) []byte { return bytes[:n] }) return r } func (r *HashSum) LastBytes(n int) *HashSum { r.functions = append(r.functions, func(bytes []byte) []byte { return bytes[len(bytes)-n:] }) return r } ================================================ FILE: hashring.go ================================================ package hashring import ( "crypto/md5" "fmt" "sort" "strconv" ) var defaultHashFunc = func() HashFunc { hashFunc, err := NewHash(md5.New).Use(NewInt64PairHashKey) if err != nil { panic(fmt.Sprintf("failed to create defaultHashFunc: %s", err.Error())) } return hashFunc }() type HashKey interface { Less(other HashKey) bool } type HashKeyOrder []HashKey func (h HashKeyOrder) Len() int { return len(h) } func (h HashKeyOrder) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h HashKeyOrder) Less(i, j int) bool { return h[i].Less(h[j]) } type HashFunc func([]byte) HashKey type HashRing struct { ring map[HashKey]string sortedKeys []HashKey nodes []string weights map[string]int hashFunc HashFunc } type Uint32HashKey uint32 func (k Uint32HashKey) Less(other HashKey) bool { return k < other.(Uint32HashKey) } func New(nodes []string) *HashRing { return NewWithHash(nodes, defaultHashFunc) } func NewWithHash( nodes []string, hashKey HashFunc, ) *HashRing { hashRing := &HashRing{ ring: make(map[HashKey]string), sortedKeys: make([]HashKey, 0), nodes: nodes, weights: make(map[string]int), hashFunc: hashKey, } hashRing.generateCircle() return hashRing } func NewWithWeights(weights map[string]int) *HashRing { return NewWithHashAndWeights(weights, defaultHashFunc) } func NewWithHashAndWeights( weights map[string]int, hashFunc HashFunc, ) *HashRing { nodes := make([]string, 0, len(weights)) for node := range weights { nodes = append(nodes, node) } hashRing := &HashRing{ ring: make(map[HashKey]string), sortedKeys: make([]HashKey, 0), nodes: nodes, weights: weights, hashFunc: hashFunc, } hashRing.generateCircle() return hashRing } func (h *HashRing) Size() int { return len(h.nodes) } func (h *HashRing) UpdateWithWeights(weights map[string]int) { nodesChgFlg := false if len(weights) != len(h.weights) { nodesChgFlg = true } else { for node, newWeight := range weights { oldWeight, ok := h.weights[node] if !ok || oldWeight != newWeight { nodesChgFlg = true break } } } if nodesChgFlg { newhring := NewWithHashAndWeights(weights, h.hashFunc) h.weights = newhring.weights h.nodes = newhring.nodes h.ring = newhring.ring h.sortedKeys = newhring.sortedKeys } } func (h *HashRing) generateCircle() { totalWeight := 0 for _, node := range h.nodes { if weight, ok := h.weights[node]; ok { totalWeight += weight } else { totalWeight += 1 h.weights[node] = 1 } } for _, node := range h.nodes { weight := h.weights[node] for j := 0; j < weight; j++ { nodeKey := node + "-" + strconv.FormatInt(int64(j), 10) key := h.hashFunc([]byte(nodeKey)) h.ring[key] = node h.sortedKeys = append(h.sortedKeys, key) } } sort.Sort(HashKeyOrder(h.sortedKeys)) } func (h *HashRing) GetNode(stringKey string) (node string, ok bool) { pos, ok := h.GetNodePos(stringKey) if !ok { return "", false } return h.ring[h.sortedKeys[pos]], true } func (h *HashRing) GetNodePos(stringKey string) (pos int, ok bool) { if len(h.ring) == 0 { return 0, false } key := h.GenKey(stringKey) nodes := h.sortedKeys pos = sort.Search(len(nodes), func(i int) bool { return key.Less(nodes[i]) }) if pos == len(nodes) { // Wrap the search, should return First node return 0, true } else { return pos, true } } func (h *HashRing) GenKey(key string) HashKey { return h.hashFunc([]byte(key)) } // GetNodes iterates over the hash ring and returns the nodes in the order // which is determined by the key. GetNodes is thread safe if the hash // which was used to configure the hash ring is thread safe. func (h *HashRing) GetNodes(stringKey string, size int) (nodes []string, ok bool) { pos, ok := h.GetNodePos(stringKey) if !ok { return nil, false } if size > len(h.nodes) { return nil, false } returnedValues := make(map[string]bool, size) //mergedSortedKeys := append(h.sortedKeys[pos:], h.sortedKeys[:pos]...) resultSlice := make([]string, 0, size) for i := pos; i < pos+len(h.sortedKeys); i++ { key := h.sortedKeys[i%len(h.sortedKeys)] val := h.ring[key] if !returnedValues[val] { returnedValues[val] = true resultSlice = append(resultSlice, val) } if len(returnedValues) == size { break } } return resultSlice, len(resultSlice) == size } func (h *HashRing) AddNode(node string) *HashRing { return h.AddWeightedNode(node, 1) } func (h *HashRing) AddWeightedNode(node string, weight int) *HashRing { if weight <= 0 { return h } if _, ok := h.weights[node]; ok { return h } nodes := make([]string, len(h.nodes), len(h.nodes)+1) copy(nodes, h.nodes) nodes = append(nodes, node) weights := make(map[string]int) for eNode, eWeight := range h.weights { weights[eNode] = eWeight } weights[node] = weight hashRing := &HashRing{ ring: make(map[HashKey]string), sortedKeys: make([]HashKey, 0), nodes: nodes, weights: weights, hashFunc: h.hashFunc, } hashRing.generateCircle() return hashRing } func (h *HashRing) UpdateWeightedNode(node string, weight int) *HashRing { if weight <= 0 { return h } /* node is not need to update for node is not existed or weight is not changed */ if oldWeight, ok := h.weights[node]; (!ok) || (ok && oldWeight == weight) { return h } nodes := make([]string, len(h.nodes)) copy(nodes, h.nodes) weights := make(map[string]int) for eNode, eWeight := range h.weights { weights[eNode] = eWeight } weights[node] = weight hashRing := &HashRing{ ring: make(map[HashKey]string), sortedKeys: make([]HashKey, 0), nodes: nodes, weights: weights, hashFunc: h.hashFunc, } hashRing.generateCircle() return hashRing } func (h *HashRing) RemoveNode(node string) *HashRing { /* if node isn't exist in hashring, don't refresh hashring */ if _, ok := h.weights[node]; !ok { return h } nodes := make([]string, 0) for _, eNode := range h.nodes { if eNode != node { nodes = append(nodes, eNode) } } weights := make(map[string]int) for eNode, eWeight := range h.weights { if eNode != node { weights[eNode] = eWeight } } hashRing := &HashRing{ ring: make(map[HashKey]string), sortedKeys: make([]HashKey, 0), nodes: nodes, weights: weights, hashFunc: h.hashFunc, } hashRing.generateCircle() return hashRing } ================================================ FILE: hashring_test.go ================================================ package hashring import ( "fmt" "reflect" "testing" "github.com/stretchr/testify/assert" ) func expectWeights(t *testing.T, ring *HashRing, expectedWeights map[string]int) { weightsEquality := reflect.DeepEqual(ring.weights, expectedWeights) if !weightsEquality { t.Error("Weights expected", expectedWeights, "but got", ring.weights) } } type testPair struct { key string node string } type testNodes struct { key string nodes []string } func assert2Nodes(t *testing.T, prefix string, ring *HashRing, data []testNodes) { t.Run(prefix, func(t *testing.T) { allActual := make([]string, 0) allExpected := make([]string, 0) for _, pair := range data { nodes, ok := ring.GetNodes(pair.key, 2) if assert.True(t, ok) { allActual = append(allActual, fmt.Sprintf("%s - %v", pair.key, nodes)) allExpected = append(allExpected, fmt.Sprintf("%s - %v", pair.key, pair.nodes)) } } assert.Equal(t, allExpected, allActual) }) } func assertNodes(t *testing.T, prefix string, ring *HashRing, allExpected []testPair) { t.Run(prefix, func(t *testing.T) { allActual := make([]testPair, 0) for _, pair := range allExpected { node, ok := ring.GetNode(pair.key) if assert.True(t, ok) { allActual = append(allActual, testPair{key: pair.key, node: node}) } } }) } func expectNodesABC(t *testing.T, prefix string, ring *HashRing) { assertNodes(t, prefix, ring, []testPair{ {"test", "a"}, {"test", "a"}, {"test1", "b"}, {"test2", "b"}, {"test3", "c"}, {"test4", "a"}, {"test5", "c"}, {"aaaa", "c"}, {"bbbb", "a"}, }) } func expectNodeRangesABC(t *testing.T, prefix string, ring *HashRing) { assert2Nodes(t, prefix, ring, []testNodes{ {"test", []string{"a", "c"}}, {"test", []string{"a", "c"}}, {"test1", []string{"b", "a"}}, {"test2", []string{"b", "a"}}, {"test3", []string{"c", "b"}}, {"test4", []string{"a", "c"}}, {"test5", []string{"c", "b"}}, {"aaaa", []string{"c", "b"}}, {"bbbb", []string{"a", "c"}}, }) } func expectNodesABCD(t *testing.T, prefix string, ring *HashRing) { assertNodes(t, prefix, ring, []testPair{ {"test", "d"}, {"test", "d"}, {"test1", "b"}, {"test2", "b"}, {"test3", "c"}, {"test4", "d"}, {"test5", "c"}, {"aaaa", "c"}, {"bbbb", "d"}, }) } func TestNew(t *testing.T) { nodes := []string{"a", "b", "c"} ring := New(nodes) expectNodesABC(t, "TestNew_1_", ring) expectNodeRangesABC(t, "", ring) } func TestNewEmpty(t *testing.T) { nodes := []string{} ring := New(nodes) node, ok := ring.GetNode("test") if ok || node != "" { t.Error("GetNode(test) expected (\"\", false) but got (", node, ",", ok, ")") } nodes, rok := ring.GetNodes("test", 2) if rok || !(len(nodes) == 0) { t.Error("GetNode(test) expected ( [], false ) but got (", nodes, ",", rok, ")") } } func TestForMoreNodes(t *testing.T) { nodes := []string{"a", "b", "c"} ring := New(nodes) nodes, ok := ring.GetNodes("test", 5) if ok || !(len(nodes) == 0) { t.Error("GetNode(test) expected ( [], false ) but got (", nodes, ",", ok, ")") } } func TestForEqualNodes(t *testing.T) { nodes := []string{"a", "b", "c"} ring := New(nodes) nodes, ok := ring.GetNodes("test", 3) if !ok && (len(nodes) == 3) { t.Error("GetNode(test) expected ( [a b c], true ) but got (", nodes, ",", ok, ")") } } func TestNewSingle(t *testing.T) { nodes := []string{"a"} ring := New(nodes) assertNodes(t, "", ring, []testPair{ {"test", "a"}, {"test", "a"}, {"test1", "a"}, {"test2", "a"}, {"test3", "a"}, {"test14", "a"}, {"test15", "a"}, {"test16", "a"}, {"test17", "a"}, {"test18", "a"}, {"test19", "a"}, {"test20", "a"}, }) } func TestNewWeighted(t *testing.T) { weights := make(map[string]int) weights["a"] = 1 weights["b"] = 2 weights["c"] = 1 ring := NewWithWeights(weights) assertNodes(t, "", ring, []testPair{ {"test", "b"}, {"test", "b"}, {"test1", "b"}, {"test2", "b"}, {"test3", "c"}, {"test4", "b"}, {"test5", "c"}, {"aaaa", "c"}, {"bbbb", "b"}, }) assert2Nodes(t, "", ring, []testNodes{ {"test", []string{"b", "a"}}, }) } func TestRemoveNode(t *testing.T) { nodes := []string{"a", "b", "c"} ring := New(nodes) ring = ring.RemoveNode("b") assertNodes(t, "", ring, []testPair{ {"test", "a"}, {"test", "a"}, {"test1", "a"}, {"test2", "a"}, {"test3", "c"}, {"test4", "a"}, {"test5", "c"}, {"aaaa", "c"}, {"bbbb", "a"}, }) assert2Nodes(t, "", ring, []testNodes{ {"test", []string{"a", "c"}}, }) } func TestAddNode(t *testing.T) { nodes := []string{"a", "c"} ring := New(nodes) ring = ring.AddNode("b") expectNodesABC(t, "TestAddNode_1_", ring) defaultWeights := map[string]int{ "a": 1, "b": 1, "c": 1, } expectWeights(t, ring, defaultWeights) } func TestAddNode2(t *testing.T) { nodes := []string{"a", "c"} ring := New(nodes) ring = ring.AddNode("b") ring = ring.AddNode("b") expectNodesABC(t, "TestAddNode2_", ring) expectNodeRangesABC(t, "", ring) } func TestAddNode3(t *testing.T) { nodes := []string{"a", "b", "c"} ring := New(nodes) ring = ring.AddNode("d") expectNodesABCD(t, "TestAddNode3_1_", ring) ring = ring.AddNode("e") assertNodes(t, "TestAddNode3_2_", ring, []testPair{ {"test", "d"}, {"test", "d"}, {"test1", "b"}, {"test2", "e"}, {"test3", "c"}, {"test4", "d"}, {"test5", "c"}, {"aaaa", "c"}, {"bbbb", "d"}, }) assert2Nodes(t, "", ring, []testNodes{ {"test", []string{"d", "a"}}, }) ring = ring.AddNode("f") assertNodes(t, "TestAddNode3_3_", ring, []testPair{ {"test", "d"}, {"test", "d"}, {"test1", "b"}, {"test2", "e"}, {"test3", "c"}, {"test4", "d"}, {"test5", "c"}, {"aaaa", "c"}, {"bbbb", "d"}, }) assert2Nodes(t, "", ring, []testNodes{ {"test", []string{"d", "a"}}, }) } func TestDuplicateNodes(t *testing.T) { nodes := []string{"a", "a", "a", "a", "b"} ring := New(nodes) assertNodes(t, "TestDuplicateNodes_", ring, []testPair{ {"test", "a"}, {"test", "a"}, {"test1", "b"}, {"test2", "b"}, {"test3", "b"}, {"test4", "a"}, {"test5", "b"}, {"aaaa", "b"}, {"bbbb", "a"}, }) } func TestAddWeightedNode(t *testing.T) { nodes := []string{"a", "c"} ring := New(nodes) ring = ring.AddWeightedNode("b", 0) ring = ring.AddWeightedNode("b", 2) ring = ring.AddWeightedNode("b", 2) assertNodes(t, "TestAddWeightedNode_", ring, []testPair{ {"test", "b"}, {"test", "b"}, {"test1", "b"}, {"test2", "b"}, {"test3", "c"}, {"test4", "b"}, {"test5", "c"}, {"aaaa", "c"}, {"bbbb", "b"}, }) assert2Nodes(t, "", ring, []testNodes{ {"test", []string{"b", "a"}}, }) } func TestUpdateWeightedNode(t *testing.T) { nodes := []string{"a", "c"} ring := New(nodes) ring = ring.AddWeightedNode("b", 1) ring = ring.UpdateWeightedNode("b", 2) ring = ring.UpdateWeightedNode("b", 2) ring = ring.UpdateWeightedNode("b", 0) ring = ring.UpdateWeightedNode("d", 2) assertNodes(t, "TestUpdateWeightedNode_", ring, []testPair{ {"test", "b"}, {"test", "b"}, {"test1", "b"}, {"test2", "b"}, {"test3", "c"}, {"test4", "b"}, {"test5", "c"}, {"aaaa", "c"}, {"bbbb", "b"}, }) assert2Nodes(t, "", ring, []testNodes{ {"test", []string{"b", "a"}}, }) } func TestRemoveAddNode(t *testing.T) { nodes := []string{"a", "b", "c"} ring := New(nodes) expectNodesABC(t, "1_", ring) expectNodeRangesABC(t, "2_", ring) ring = ring.RemoveNode("b") assertNodes(t, "3_", ring, []testPair{ {"test", "a"}, {"test", "a"}, {"test1", "a"}, {"test2", "a"}, {"test3", "c"}, {"test4", "a"}, {"test5", "c"}, {"aaaa", "c"}, {"bbbb", "a"}, }) assert2Nodes(t, "4_", ring, []testNodes{ {"test", []string{"a", "c"}}, {"test", []string{"a", "c"}}, {"test1", []string{"a", "c"}}, {"test2", []string{"a", "c"}}, {"test3", []string{"c", "a"}}, {"test4", []string{"a", "c"}}, {"test5", []string{"c", "a"}}, {"aaaa", []string{"c", "a"}}, {"bbbb", []string{"a", "c"}}, }) ring = ring.AddNode("b") expectNodesABC(t, "5_", ring) expectNodeRangesABC(t, "6_", ring) } func TestRemoveAddWeightedNode(t *testing.T) { weights := make(map[string]int) weights["a"] = 1 weights["b"] = 2 weights["c"] = 1 ring := NewWithWeights(weights) expectWeights(t, ring, weights) assertNodes(t, "1_", ring, []testPair{ {"test", "b"}, {"test", "b"}, {"test1", "b"}, {"test2", "b"}, {"test3", "c"}, {"test4", "b"}, {"test5", "c"}, {"aaaa", "c"}, {"bbbb", "b"}, }) assert2Nodes(t, "2_", ring, []testNodes{ {"test", []string{"b", "a"}}, {"test", []string{"b", "a"}}, {"test1", []string{"b", "a"}}, {"test2", []string{"b", "a"}}, {"test3", []string{"c", "b"}}, {"test4", []string{"b", "a"}}, {"test5", []string{"c", "b"}}, {"aaaa", []string{"c", "b"}}, {"bbbb", []string{"b", "a"}}, }) ring = ring.RemoveNode("c") delete(weights, "c") expectWeights(t, ring, weights) assertNodes(t, "3_", ring, []testPair{ {"test", "b"}, {"test", "b"}, {"test1", "b"}, {"test2", "b"}, {"test3", "b"}, {"test4", "b"}, {"test5", "b"}, {"aaaa", "b"}, {"bbbb", "b"}, }) assert2Nodes(t, "4_", ring, []testNodes{ {"test", []string{"b", "a"}}, {"test", []string{"b", "a"}}, {"test1", []string{"b", "a"}}, {"test2", []string{"b", "a"}}, {"test3", []string{"b", "a"}}, {"test4", []string{"b", "a"}}, {"test5", []string{"b", "a"}}, {"aaaa", []string{"b", "a"}}, {"bbbb", []string{"b", "a"}}, }) } func TestAddRemoveNode(t *testing.T) { nodes := []string{"a", "b", "c"} ring := New(nodes) ring = ring.AddNode("d") expectNodesABCD(t, "1_", ring) assert2Nodes(t, "2_", ring, []testNodes{ {"test", []string{"d", "a"}}, {"test", []string{"d", "a"}}, {"test1", []string{"b", "d"}}, {"test2", []string{"b", "d"}}, {"test3", []string{"c", "b"}}, {"test4", []string{"d", "a"}}, {"test5", []string{"c", "b"}}, {"aaaa", []string{"c", "b"}}, {"bbbb", []string{"d", "a"}}, }) ring = ring.AddNode("e") assertNodes(t, "3_", ring, []testPair{ {"test", "a"}, {"test", "a"}, {"test1", "b"}, {"test2", "b"}, {"test3", "c"}, {"test4", "c"}, {"test5", "a"}, {"aaaa", "b"}, {"bbbb", "e"}, }) assert2Nodes(t, "4_", ring, []testNodes{ {"test", []string{"d", "a"}}, {"test", []string{"d", "a"}}, {"test1", []string{"b", "d"}}, {"test2", []string{"e", "b"}}, {"test3", []string{"c", "e"}}, {"test4", []string{"d", "a"}}, {"test5", []string{"c", "e"}}, {"aaaa", []string{"c", "e"}}, {"bbbb", []string{"d", "a"}}, }) ring = ring.AddNode("f") assertNodes(t, "5_", ring, []testPair{ {"test", "a"}, {"test", "a"}, {"test1", "b"}, {"test2", "f"}, {"test3", "f"}, {"test4", "c"}, {"test5", "f"}, {"aaaa", "b"}, {"bbbb", "e"}, }) assert2Nodes(t, "6_", ring, []testNodes{ {"test", []string{"d", "a"}}, {"test", []string{"d", "a"}}, {"test1", []string{"b", "d"}}, {"test2", []string{"e", "f"}}, {"test3", []string{"c", "e"}}, {"test4", []string{"d", "a"}}, {"test5", []string{"c", "e"}}, {"aaaa", []string{"c", "e"}}, {"bbbb", []string{"d", "a"}}, }) ring = ring.RemoveNode("e") assertNodes(t, "7_", ring, []testPair{ {"test", "a"}, {"test", "a"}, {"test1", "b"}, {"test2", "f"}, {"test3", "f"}, {"test4", "c"}, {"test5", "f"}, {"aaaa", "b"}, {"bbbb", "f"}, }) assert2Nodes(t, "8_", ring, []testNodes{ {"test", []string{"d", "a"}}, {"test", []string{"d", "a"}}, {"test1", []string{"b", "d"}}, {"test2", []string{"f", "b"}}, {"test3", []string{"c", "f"}}, {"test4", []string{"d", "a"}}, {"test5", []string{"c", "f"}}, {"aaaa", []string{"c", "f"}}, {"bbbb", []string{"d", "a"}}, }) ring = ring.RemoveNode("f") expectNodesABCD(t, "TestAddRemoveNode_5_", ring) assert2Nodes(t, "", ring, []testNodes{ {"test", []string{"d", "a"}}, {"test", []string{"d", "a"}}, {"test1", []string{"b", "d"}}, {"test2", []string{"b", "d"}}, {"test3", []string{"c", "b"}}, {"test4", []string{"d", "a"}}, {"test5", []string{"c", "b"}}, {"aaaa", []string{"c", "b"}}, {"bbbb", []string{"d", "a"}}, }) ring = ring.RemoveNode("d") expectNodesABC(t, "TestAddRemoveNode_6_", ring) expectNodeRangesABC(t, "", ring) } ================================================ FILE: key.go ================================================ package hashring import ( "encoding/binary" "fmt" ) type Int64PairHashKey struct { High int64 Low int64 } func (k *Int64PairHashKey) Less(other HashKey) bool { o := other.(*Int64PairHashKey) if k.High < o.High { return true } return k.High == o.High && k.Low < o.Low } func NewInt64PairHashKey(bytes []byte) (HashKey, error) { const expected = 16 if len(bytes) != expected { return nil, fmt.Errorf( "expected %d bytes, got %d bytes", expected, len(bytes), ) } return &Int64PairHashKey{ High: int64(binary.LittleEndian.Uint64(bytes[:8])), Low: int64(binary.LittleEndian.Uint64(bytes[8:])), }, nil }