Skip to content

Commit

Permalink
initial hashring
Browse files Browse the repository at this point in the history
  • Loading branch information
nikkicoon committed Jun 25, 2024
0 parents commit 4b78d4e
Show file tree
Hide file tree
Showing 13 changed files with 447 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/go-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: go-coverage

on:
push:
branches: [ "default" ]
pull_request:
branches: [ "default" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22'

- name: test coverage
run: go test ./... -coverprofile=cover.out && go tool cover -func cover.out
25 changes: 25 additions & 0 deletions .github/workflows/gocheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: gocheck

on:
push:
branches: [ "default" ]
pull_request:
branches: [ "default" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22'

- name: Test pkg
run: cd pkg && go test -v .
25 changes: 25 additions & 0 deletions .github/workflows/gofmt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: gofmt

on:
push:
branches: [ "default" ]
pull_request:
branches: [ "default" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22'

- name: gofmt check
run: gofmt -l -s -d .
26 changes: 26 additions & 0 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Golangci-lint

on:
push:
branches: [ "default" ]
pull_request:
branches: [ "default" ]

permissions:
contents: read
pull-requests: read

jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=30m --issues-exit-code=0
25 changes: 25 additions & 0 deletions .github/workflows/govet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: govet

on:
push:
branches: [ "default" ]
pull_request:
branches: [ "default" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22'

- name: go vet check
run: go vet pkg/*.go
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.test
*.out
go.work
.idea
25 changes: 25 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
BSD 2-Clause License

Copyright (c) 2024, Nikita Ronja Gillmann <[email protected]>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1 change: 1 addition & 0 deletions README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
small consistent hashring implementation in go using bytes, 2-clause bsd licensed.
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/nikkicoon/consistenthashring

go 1.22

require github.com/stretchr/testify v1.9.0

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
126 changes: 126 additions & 0 deletions pkg/hashring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package pkg

import (
"bytes"
"fmt"
"slices"
"strconv"
)

type ConsistentHashRing struct {
// Number of Labels per entry (vnodes per node)
Labels int
// Slice of Keys
Keys [][]byte
// Slice of Node
Nodes []Node
}

func NewConsistentHashRing(labels int) *ConsistentHashRing {
return &ConsistentHashRing{
Labels: labels,
}
}

// String approximates a pretty-print of the contents of the ConsistentHashRing
// XXX: Maybe do a not-pretty-print variant as a basis for a PrettyPrint function.
func (c *ConsistentHashRing) String() string {
var a, b string
for _, v := range c.Nodes {
a += "\t\t\t" + v.String()
}
for _, v := range c.Keys {
b += "\t\t\t" + string(v) + "\n"
}
return fmt.Sprintf("{\n\tLabels: %d\n\tKeys: {\n%s\t}\n\tNodes: {\n%s\t}\n}", c.Labels, b, a)
}

type Node struct {
Hash []byte
Host string
IP string
SID string
}

func NewNode(host string, ip string, sid string) Node {
return Node{
Hash: []byte{},
Host: host,
IP: ip,
SID: sid,
}
}

func (n Node) String() string {
result := make([]byte, len(n.Hash))
buff := bytes.NewBuffer(result)
for _, b := range n.Hash {
fmt.Fprintf(buff, "0x%02x ", b)
}
return fmt.Sprintf("Hash: {%s} Host: %s IP: %s SID: %s\n", buff.String(), n.Host, n.IP, n.SID)
}

// Add adds a node given its name.
// The given nodeName is hashed among the number of labels.
func (c *ConsistentHashRing) Add(nodeName string, node Node) {
for i := 0; i < c.Labels; i++ {
hash := CalculateHash(nodeName + strconv.Itoa(i))
node.Hash = hash
c.Nodes = append(c.Nodes, node)
c.Keys = SortedInsertByte(c.Keys, hash)
}
}

// Get returns a node given a key.
// The node replica with a hash value nearest but not
// less than that of the given name is returned. If the hash
// of the given name is greater than the greatest hash,
// return the lowest hashed node.
// If the Hashring is empty or any other case happens,
// return an empty Node type.
func (c *ConsistentHashRing) Get(keyname string) *Node {
// if empty, return empty
if len(c.Nodes) == 0 {
return &Node{}
}
hash := CalculateHash(keyname)
idx, ok := binarySearchBytes(c.Keys, hash, 0, len(c.Keys)-1)
if !ok {
//return &Node{}
idx = 0
}
if idx == len(c.Keys) {
idx = 0
}
var x []byte
if len(c.Keys) > 0 {
x = c.Keys[idx]
} else {
return &Node{}
}
for k, v := range c.Nodes {
if bytes.Equal(v.Hash, x) {
return &c.Nodes[k]
}
}
return &Node{}
}

// Delete deletes a node given its name.
func (c *ConsistentHashRing) Delete(nodeName string) {
for i := 0; i < c.Labels; i++ {
hash := CalculateHash(nodeName + strconv.Itoa(i))
// delete from c.Nodes where hash matches
for k, v := range c.Nodes {
if bytes.Equal(v.Hash, hash) {
c.Nodes = slices.Delete(c.Nodes, k, k+1)
}
}
// delete from c.Keys
for k, val := range c.Keys {
if bytes.Equal(val, hash) {
c.Keys = slices.Delete(c.Keys, k, k+1)
}
}
}
}
69 changes: 69 additions & 0 deletions pkg/hashring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package pkg_test

import (
"github.com/nikkicoon/consistenthashring/pkg"
"github.com/stretchr/testify/assert"
"testing"
)

func TestCalculateHash(t *testing.T) {
res := pkg.CalculateHash("test")
assert.Equal(t, []byte{0xa9, 0x4a, 0x8f, 0xe5, 0xcc, 0xb1, 0x9b, 0xa6, 0x1c, 0x4c, 0x8, 0x73, 0xd3, 0x91, 0xe9, 0x87, 0x98, 0x2f, 0xbb, 0xd3}, res)
}

func BenchmarkCalculateHash(b *testing.B) {
for n := 0; n < 6000000; n++ {
pkg.CalculateHash("test")
}
b.Elapsed()
}

func BenchmarkCalculateHashGoRoutines(b *testing.B) {
for n := 0; n < 6000000; n++ {
go pkg.CalculateHash("test")
}
b.Elapsed()
}

func TestConsistentHashRing_Get(t *testing.T) {
t.Parallel()
cr := pkg.NewConsistentHashRing(5)
n := pkg.NewNode("foobar.de", "127.0.0.1", "0")
cr.Add("node0", n)
hashesNode0 := [][]byte{
{0x25, 0xc3, 0xcf, 0xcb, 0x8b, 0x52, 0x0d, 0xbc, 0x5f, 0x85, 0xe9, 0xfb, 0x16, 0xe8, 0x6b, 0xaa, 0x13, 0x9f, 0x7a, 0x99},
{0xaa, 0x99, 0x2f, 0xdc, 0x24, 0x46, 0xe7, 0x27, 0xc3, 0x30, 0xa3, 0xcc, 0x4b, 0x9c, 0x2f, 0x46, 0x7e, 0x43, 0x8d, 0x6e},
{0x68, 0x4e, 0x61, 0xda, 0xe6, 0xe5, 0x5c, 0xfc, 0x61, 0x1a, 0x2b, 0xbd, 0xa6, 0x99, 0x4a, 0x84, 0xfd, 0xab, 0x91, 0x4e},
{0x43, 0xc9, 0x45, 0xef, 0x85, 0x95, 0x9c, 0x47, 0x12, 0x1d, 0x74, 0x31, 0x0c, 0xfa, 0x9f, 0xa9, 0x34, 0xa7, 0x30, 0xb3},
{0x28, 0x4b, 0xff, 0x8f, 0x21, 0xb8, 0x41, 0x0c, 0x4e, 0xb8, 0x84, 0x49, 0x26, 0xe6, 0x63, 0x60, 0x87, 0x99, 0xd0, 0xc2}}
q := cr.Get("node0")
if q != nil {
assert.Equal(t, "foobar.de", q.Host)
assert.Equal(t, 5, len(cr.Nodes))
for i := 0; i < len(cr.Nodes); i++ {
assert.Equal(t, hashesNode0[i], cr.Nodes[i].Hash)
}
} else {
panic("some error with get('node0')")
}
cr.Delete("node0")
l := cr.Get("node9000")
assert.IsType(t, &pkg.Node{}, l)
assert.Equal(t, pkg.Node{}, *l)
}

func TestConsistentHashRing_Add(t *testing.T) {
cr := pkg.NewConsistentHashRing(2000)
cr.Add("node0", pkg.NewNode("host0.domain.tld", "123.123.123.123", "0"))
cr.Add("node1", pkg.NewNode("host1.domain.tld", "123.123.123.123", "0"))
cr.Add("node2", pkg.NewNode("host2.domain.tld", "123.123.123.123", "0"))
cr.Add("node3", pkg.NewNode("host3.domain.tld", "123.123.123.123", "0"))
cr.Add("node4", pkg.NewNode("host4.domain.tld", "123.123.123.123", "0"))
cr.Add("node5", pkg.NewNode("host5.domain.tld", "123.123.123.123", "0"))
for _, res := range cr.Nodes {
assert.NotZero(t, res.Hash)
assert.NotNil(t, res.Hash)
}
assert.NotZero(t, len(cr.Nodes))
assert.NotZero(t, len(cr.Keys))
}
Loading

0 comments on commit 4b78d4e

Please sign in to comment.