├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── History.txt ├── README.md ├── Rakefile ├── benchmark └── benchmark.rb ├── consistent-hashing.gemspec ├── docs ├── _config.yml ├── images │ ├── bg_hr.png │ ├── blacktocat.png │ ├── icon_download.png │ └── sprite_download.png ├── index.md └── stylesheets │ ├── pygment_trac.css │ └── stylesheet.css ├── lib ├── consistent_hashing.rb └── consistent_hashing │ ├── avl_tree.rb │ ├── ring.rb │ └── virtual_point.rb ├── test ├── consistent_hashing │ ├── test_avl_tree.rb │ ├── test_ring.rb │ └── test_virtual_point.rb └── test_consistent_hashing.rb └── version.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # RubyMine 2 | .idea 3 | *.gem 4 | 5 | # gem/bones 6 | announcement.txt 7 | coverage 8 | doc 9 | pkg 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1 4 | - 2.2 5 | - 2.3 6 | - 2.4 7 | - 2.5 8 | - jruby-head 9 | notifications: 10 | recipients: 11 | - liebler.dominik@gmail.com 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rake' 3 | gem 'bones' 4 | gem 'simplecov' 5 | gem 'avl_tree' 6 | gem 'test-unit' 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | atomic (1.1.101) 5 | atomic (1.1.101-java) 6 | avl_tree (1.2.1) 7 | atomic (~> 1.1) 8 | bones (3.8.4) 9 | little-plugger (~> 1.1) 10 | loquacious (~> 1.9) 11 | rake (~> 12.0) 12 | rdoc (~> 5.0) 13 | docile (1.3.1) 14 | json (2.1.0) 15 | json (2.1.0-java) 16 | little-plugger (1.1.4) 17 | loquacious (1.9.1) 18 | power_assert (1.1.3) 19 | rake (12.3.2) 20 | rdoc (5.1.0) 21 | simplecov (0.16.1) 22 | docile (~> 1.1) 23 | json (>= 1.8, < 3) 24 | simplecov-html (~> 0.10.0) 25 | simplecov-html (0.10.2) 26 | test-unit (3.2.9) 27 | power_assert 28 | 29 | PLATFORMS 30 | java 31 | ruby 32 | 33 | DEPENDENCIES 34 | avl_tree 35 | bones 36 | rake 37 | simplecov 38 | test-unit 39 | 40 | BUNDLED WITH 41 | 1.17.2 42 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 2.0.0 / 2019-08-26 2 | 3 | * fixed probable performance issue with Ring.<< and Ring.delete which could occur using a large Ring 4 | 5 | == 0.2.1 / 2013-04-27 6 | 7 | * fixed build, removed bones gem dependency 8 | 9 | == 0.2.0 / 2012-04-15 10 | 11 | * added AVLTree dependency 12 | 13 | == 0.1.0 / 2012-04-15 14 | 15 | * introduced the VirtualPoint class 16 | * nodes can now consist of arbitrary data 17 | * added methods to get all nodes and all virtual points on the hash ring 18 | 19 | == 0.0.1 / 2012-04-15 20 | 21 | * Birthday! -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # consistent-hashing 2 | 3 | A generic implementation of the Consistent Hashing algorithm using an AVL tree. 4 | 5 | *2019-10-19* I archived this project as there hasn't been any recent activity and I have moved on to other projects. You can still download and use it though. 6 | 7 | ## Features 8 | 9 | * set number of replicas to create multiple virtual points in the ring for each node 10 | * nodes can be arbitrary data (e.g. a Memcache client instance) 11 | * fast performance through using an AVL tree internally 12 | 13 | ## Examples 14 | 15 | ```ruby 16 | require 'consistent_hashing' 17 | 18 | ring = ConsistentHashing::Ring.new 19 | ring << "192.168.1.101" 20 | ring << "192.168.1.102" 21 | 22 | ring.node_for("foobar") # => 192.168.1.101 23 | ring.delete("192.168.1.101") 24 | 25 | # after removing 192.168.1.101, all keys previously mapped to it move clockwise to 26 | # the next node 27 | ring.node_for("foobar") # => 192.168.1.102 28 | 29 | ring.nodes # => ["192.168.1.101", "192.168.1.102"] 30 | ring.points # => [#, #, ...] 31 | ``` 32 | 33 | ## Installation 34 | 35 | * `[sudo] gem install consistent-hashing` 36 | 37 | ## Author 38 | 39 | Original author: Dominik Liebler 40 | 41 | ## License 42 | 43 | (The MIT License) 44 | 45 | Copyright (c) 2013 - 2019 Dominik Liebler 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of this software and associated documentation files (the 49 | 'Software'), to deal in the Software without restriction, including 50 | without limitation the rights to use, copy, modify, merge, publish, 51 | distribute, sublicense, and/or sell copies of the Software, and to 52 | permit persons to whom the Software is furnished to do so, subject to 53 | the following conditions: 54 | 55 | The above copyright notice and this permission notice shall be 56 | included in all copies or substantial portions of the Software. 57 | 58 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 59 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 60 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 61 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 62 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 63 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 64 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 65 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake/testtask' 3 | 4 | task :default => 'test' 5 | 6 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) 7 | require "consistent_hashing" 8 | 9 | gem_name = "consistent-hashing-#{ConsistentHashing::VERSION}.gem" 10 | 11 | namespace :gem do 12 | desc "clean previously generated gems" 13 | task :clean do 14 | system "rm -f *.gem" 15 | end 16 | 17 | desc "build gem" 18 | task :build => [:clean, :test] do 19 | system "gem build consistent-hashing.gemspec" 20 | end 21 | 22 | desc "install gem" 23 | task :install => :build do 24 | system "gem install #{gem_name}" 25 | end 26 | 27 | desc "release to rubygems.org" 28 | task :release => :build do 29 | system "gem push #{gem_name}" 30 | end 31 | end 32 | 33 | Rake::TestTask.new do |t| 34 | t.libs = ["lib"] 35 | t.warning = true 36 | t.verbose = true 37 | t.test_files = FileList['test/**/test_*.rb'] 38 | end 39 | -------------------------------------------------------------------------------- /benchmark/benchmark.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.join(File.dirname(__FILE__), %w{.. lib})) 2 | 3 | require 'benchmark' 4 | require 'ipaddr' 5 | require 'consistent_hashing' 6 | 7 | def ip(offset) 8 | address = IPAddr.new('10.0.0.0').to_i + offset 9 | [24, 16, 8, 0].collect {|b| (address >> b) & 255}.join('.') 10 | end 11 | 12 | def rand_client_id() 13 | (rand * 1_000_000).to_int 14 | end 15 | 16 | def benchmark_insertions_lookups() 17 | # The initial ring implementation using a combination of hash and sorted list 18 | # had the following results when benchmarked: 19 | # user system total real 20 | # Insertions: 1.260000 0.000000 1.260000 ( 1.259346) 21 | # Look ups: 20.080000 0.020000 20.100000 ( 20.111773) 22 | # 23 | # The ring implementation using an AVLTree has the following results 24 | # when benchmarked on the same system: 25 | # user system total real 26 | # Insertions: 0.060000 0.000000 0.060000 ( 0.062302) 27 | # Look ups: 1.020000 0.000000 1.020000 ( 1.028172) 28 | # 29 | # The performance improvement is ~20x for both insertions and lookups. 30 | 31 | Benchmark.bm(10) do |x| 32 | ring = ConsistentHashing::Ring.new(replicas: 200) 33 | x.report("Insertions:") {for i in 1..1_000; ring << ip(i); end} 34 | x.report("Look ups: ") do 35 | for i in 1..100_000 36 | ring.point_for(rand_client_id) 37 | end 38 | end 39 | end 40 | end 41 | 42 | benchmark_insertions_lookups 43 | -------------------------------------------------------------------------------- /consistent-hashing.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "consistent-hashing" 5 | s.version = File.read('version.txt').chomp 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Dominik Liebler"] 9 | s.date = File.mtime('version.txt') 10 | s.description = "a Consistent Hashing implementation in pure Ruby using an AVL Tree" 11 | s.email = "liebler.dominik@gmail.com" 12 | s.extra_rdoc_files = ["History.txt"] 13 | s.files = Dir.glob("{bin,lib,test,benchmark}/**/*") + %w(README.md History.txt Rakefile version.txt) 14 | s.homepage = "https://github.com/domnikl/consistent-hashing" 15 | s.rdoc_options = ["--main", "README.md"] 16 | s.require_paths = ["lib"] 17 | s.rubyforge_project = "consistent-hashing" 18 | s.rubygems_version = "1.8.16" 19 | s.summary = "" 20 | s.test_files = Dir.glob('test/**/*') 21 | 22 | if s.respond_to? :specification_version then 23 | s.specification_version = 3 24 | 25 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 26 | s.add_runtime_dependency(%q, [">= 1.1.3"]) 27 | else 28 | s.add_dependency(%q, [">= 1.1.3"]) 29 | end 30 | else 31 | s.add_dependency(%q, [">= 1.1.3"]) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: consistent-hashing 2 | description: A generic implementation of the Consistent Hashing algorithm in pure Ruby using an AVL tree. 3 | google_analytics: UA-9816149-11 4 | show_downloads: true 5 | theme: jekyll-theme-architect 6 | 7 | gems: 8 | - jekyll-mentions 9 | -------------------------------------------------------------------------------- /docs/images/bg_hr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnikl/consistent-hashing/05fa7db762f4db14d4736a848085776907071631/docs/images/bg_hr.png -------------------------------------------------------------------------------- /docs/images/blacktocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnikl/consistent-hashing/05fa7db762f4db14d4736a848085776907071631/docs/images/blacktocat.png -------------------------------------------------------------------------------- /docs/images/icon_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnikl/consistent-hashing/05fa7db762f4db14d4736a848085776907071631/docs/images/icon_download.png -------------------------------------------------------------------------------- /docs/images/sprite_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnikl/consistent-hashing/05fa7db762f4db14d4736a848085776907071631/docs/images/sprite_download.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## Features 2 | 3 | * set number of replicas to create multiple virtual points in the ring for each node 4 | * nodes can be arbitrary data (e.g. a Memcache client instance) 5 | * very good performance through using an AVL tree internally 6 | 7 | ## Examples 8 | 9 | ```ruby 10 | require 'consistent_hashing' 11 | 12 | ring = ConsistentHashing::Ring.new 13 | ring << "192.168.1.101" 14 | ring << "192.168.1.102" 15 | 16 | ring.node_for("foobar") # => 192.168.1.101 17 | ring.delete("192.168.1.101") 18 | 19 | # after removing 192.168.1.101, all keys previously mapped to it move clockwise to 20 | # the next node 21 | ring.node_for("foobar") # => 192.168.1.102 22 | 23 | ring.nodes # => ["192.168.1.101", "192.168.1.102"] 24 | ring.points # => [#, #, ...] 25 | ``` 26 | 27 | ## Install 28 | 29 | * `[sudo] gem install consistent-hashing` 30 | 31 | ## Author 32 | 33 | Original author: Dominik Liebler 34 | 35 | ## License 36 | 37 | (The MIT License) 38 | 39 | Copyright (c) 2012 - 2019 Dominik Liebler 40 | 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of this software and associated documentation files (the 43 | 'Software'), to deal in the Software without restriction, including 44 | without limitation the rights to use, copy, modify, merge, publish, 45 | distribute, sublicense, and/or sell copies of the Software, and to 46 | permit persons to whom the Software is furnished to do so, subject to 47 | the following conditions: 48 | 49 | The above copyright notice and this permission notice shall be 50 | included in all copies or substantial portions of the Software. 51 | 52 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 53 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 54 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 55 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 56 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 57 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 58 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 59 | -------------------------------------------------------------------------------- /docs/stylesheets/pygment_trac.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #f0f3f3; } 3 | .highlight .c { color: #0099FF; font-style: italic } /* Comment */ 4 | .highlight .err { color: #AA0000; background-color: #FFAAAA } /* Error */ 5 | .highlight .k { color: #006699; font-weight: bold } /* Keyword */ 6 | .highlight .o { color: #555555 } /* Operator */ 7 | .highlight .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */ 8 | .highlight .cp { color: #009999 } /* Comment.Preproc */ 9 | .highlight .c1 { color: #0099FF; font-style: italic } /* Comment.Single */ 10 | .highlight .cs { color: #0099FF; font-weight: bold; font-style: italic } /* Comment.Special */ 11 | .highlight .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */ 12 | .highlight .ge { font-style: italic } /* Generic.Emph */ 13 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 14 | .highlight .gh { color: #003300; font-weight: bold } /* Generic.Heading */ 15 | .highlight .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */ 16 | .highlight .go { color: #AAAAAA } /* Generic.Output */ 17 | .highlight .gp { color: #000099; font-weight: bold } /* Generic.Prompt */ 18 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 19 | .highlight .gu { color: #003300; font-weight: bold } /* Generic.Subheading */ 20 | .highlight .gt { color: #99CC66 } /* Generic.Traceback */ 21 | .highlight .kc { color: #006699; font-weight: bold } /* Keyword.Constant */ 22 | .highlight .kd { color: #006699; font-weight: bold } /* Keyword.Declaration */ 23 | .highlight .kn { color: #006699; font-weight: bold } /* Keyword.Namespace */ 24 | .highlight .kp { color: #006699 } /* Keyword.Pseudo */ 25 | .highlight .kr { color: #006699; font-weight: bold } /* Keyword.Reserved */ 26 | .highlight .kt { color: #007788; font-weight: bold } /* Keyword.Type */ 27 | .highlight .m { color: #FF6600 } /* Literal.Number */ 28 | .highlight .s { color: #CC3300 } /* Literal.String */ 29 | .highlight .na { color: #330099 } /* Name.Attribute */ 30 | .highlight .nb { color: #336666 } /* Name.Builtin */ 31 | .highlight .nc { color: #00AA88; font-weight: bold } /* Name.Class */ 32 | .highlight .no { color: #336600 } /* Name.Constant */ 33 | .highlight .nd { color: #9999FF } /* Name.Decorator */ 34 | .highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ 35 | .highlight .ne { color: #CC0000; font-weight: bold } /* Name.Exception */ 36 | .highlight .nf { color: #CC00FF } /* Name.Function */ 37 | .highlight .nl { color: #9999FF } /* Name.Label */ 38 | .highlight .nn { color: #00CCFF; font-weight: bold } /* Name.Namespace */ 39 | .highlight .nt { color: #330099; font-weight: bold } /* Name.Tag */ 40 | .highlight .nv { color: #003333 } /* Name.Variable */ 41 | .highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */ 42 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .highlight .mf { color: #FF6600 } /* Literal.Number.Float */ 44 | .highlight .mh { color: #FF6600 } /* Literal.Number.Hex */ 45 | .highlight .mi { color: #FF6600 } /* Literal.Number.Integer */ 46 | .highlight .mo { color: #FF6600 } /* Literal.Number.Oct */ 47 | .highlight .sb { color: #CC3300 } /* Literal.String.Backtick */ 48 | .highlight .sc { color: #CC3300 } /* Literal.String.Char */ 49 | .highlight .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */ 50 | .highlight .s2 { color: #CC3300 } /* Literal.String.Double */ 51 | .highlight .se { color: #CC3300; font-weight: bold } /* Literal.String.Escape */ 52 | .highlight .sh { color: #CC3300 } /* Literal.String.Heredoc */ 53 | .highlight .si { color: #AA0000 } /* Literal.String.Interpol */ 54 | .highlight .sx { color: #CC3300 } /* Literal.String.Other */ 55 | .highlight .sr { color: #33AAAA } /* Literal.String.Regex */ 56 | .highlight .s1 { color: #CC3300 } /* Literal.String.Single */ 57 | .highlight .ss { color: #FFCC33 } /* Literal.String.Symbol */ 58 | .highlight .bp { color: #336666 } /* Name.Builtin.Pseudo */ 59 | .highlight .vc { color: #003333 } /* Name.Variable.Class */ 60 | .highlight .vg { color: #003333 } /* Name.Variable.Global */ 61 | .highlight .vi { color: #003333 } /* Name.Variable.Instance */ 62 | .highlight .il { color: #FF6600 } /* Literal.Number.Integer.Long */ 63 | 64 | .type-csharp .highlight .k { color: #0000FF } 65 | .type-csharp .highlight .kt { color: #0000FF } 66 | .type-csharp .highlight .nf { color: #000000; font-weight: normal } 67 | .type-csharp .highlight .nc { color: #2B91AF } 68 | .type-csharp .highlight .nn { color: #000000 } 69 | .type-csharp .highlight .s { color: #A31515 } 70 | .type-csharp .highlight .sc { color: #A31515 } 71 | -------------------------------------------------------------------------------- /docs/stylesheets/stylesheet.css: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Slate Theme for Github Pages 3 | by Jason Costello, @jsncostello 4 | *******************************************************************************/ 5 | 6 | @import url(pygment_trac.css); 7 | 8 | /******************************************************************************* 9 | MeyerWeb Reset 10 | *******************************************************************************/ 11 | 12 | html, body, div, span, applet, object, iframe, 13 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 14 | a, abbr, acronym, address, big, cite, code, 15 | del, dfn, em, img, ins, kbd, q, s, samp, 16 | small, strike, strong, sub, sup, tt, var, 17 | b, u, i, center, 18 | dl, dt, dd, ol, ul, li, 19 | fieldset, form, label, legend, 20 | table, caption, tbody, tfoot, thead, tr, th, td, 21 | article, aside, canvas, details, embed, 22 | figure, figcaption, footer, header, hgroup, 23 | menu, nav, output, ruby, section, summary, 24 | time, mark, audio, video { 25 | margin: 0; 26 | padding: 0; 27 | border: 0; 28 | font: inherit; 29 | vertical-align: baseline; 30 | } 31 | 32 | /* HTML5 display-role reset for older browsers */ 33 | article, aside, details, figcaption, figure, 34 | footer, header, hgroup, menu, nav, section { 35 | display: block; 36 | } 37 | 38 | ol, ul { 39 | list-style: none; 40 | } 41 | 42 | blockquote, q { 43 | } 44 | 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | a:focus { 51 | outline: none; 52 | } 53 | 54 | /******************************************************************************* 55 | Theme Styles 56 | *******************************************************************************/ 57 | 58 | body { 59 | box-sizing: border-box; 60 | color:#373737; 61 | background: #212121; 62 | font-size: 16px; 63 | font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif; 64 | line-height: 1.5; 65 | -webkit-font-smoothing: antialiased; 66 | } 67 | 68 | h1, h2, h3, h4, h5, h6 { 69 | margin: 10px 0; 70 | font-weight: 700; 71 | color:#222222; 72 | font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif; 73 | letter-spacing: -1px; 74 | } 75 | 76 | h1 { 77 | font-size: 36px; 78 | font-weight: 700; 79 | } 80 | 81 | h2 { 82 | padding-bottom: 10px; 83 | font-size: 32px; 84 | background: url('../images/bg_hr.png') repeat-x bottom; 85 | } 86 | 87 | h3 { 88 | font-size: 24px; 89 | } 90 | 91 | h4 { 92 | font-size: 21px; 93 | } 94 | 95 | h5 { 96 | font-size: 18px; 97 | } 98 | 99 | h6 { 100 | font-size: 16px; 101 | } 102 | 103 | p { 104 | margin: 10px 0 15px 0; 105 | } 106 | 107 | footer p { 108 | color: #f2f2f2; 109 | } 110 | 111 | a { 112 | text-decoration: none; 113 | color: #007edf; 114 | text-shadow: none; 115 | 116 | transition: color 0.5s ease; 117 | transition: text-shadow 0.5s ease; 118 | -webkit-transition: color 0.5s ease; 119 | -webkit-transition: text-shadow 0.5s ease; 120 | -moz-transition: color 0.5s ease; 121 | -moz-transition: text-shadow 0.5s ease; 122 | -o-transition: color 0.5s ease; 123 | -o-transition: text-shadow 0.5s ease; 124 | -ms-transition: color 0.5s ease; 125 | -ms-transition: text-shadow 0.5s ease; 126 | } 127 | 128 | #main_content a:hover { 129 | color: #0069ba; 130 | text-shadow: #0090ff 0px 0px 2px; 131 | } 132 | 133 | footer a:hover { 134 | color: #43adff; 135 | text-shadow: #0090ff 0px 0px 2px; 136 | } 137 | 138 | em { 139 | font-style: italic; 140 | } 141 | 142 | strong { 143 | font-weight: bold; 144 | } 145 | 146 | img { 147 | position: relative; 148 | margin: 0 auto; 149 | max-width: 739px; 150 | padding: 5px; 151 | margin: 10px 0 10px 0; 152 | border: 1px solid #ebebeb; 153 | 154 | box-shadow: 0 0 5px #ebebeb; 155 | -webkit-box-shadow: 0 0 5px #ebebeb; 156 | -moz-box-shadow: 0 0 5px #ebebeb; 157 | -o-box-shadow: 0 0 5px #ebebeb; 158 | -ms-box-shadow: 0 0 5px #ebebeb; 159 | } 160 | 161 | pre, code { 162 | width: 100%; 163 | color: #222; 164 | background-color: #fff; 165 | 166 | font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; 167 | font-size: 14px; 168 | 169 | border-radius: 2px; 170 | -moz-border-radius: 2px; 171 | -webkit-border-radius: 2px; 172 | 173 | 174 | 175 | } 176 | 177 | pre { 178 | width: 100%; 179 | padding: 10px; 180 | box-shadow: 0 0 10px rgba(0,0,0,.1); 181 | overflow: auto; 182 | } 183 | 184 | code { 185 | padding: 3px; 186 | margin: 0 3px; 187 | box-shadow: 0 0 10px rgba(0,0,0,.1); 188 | } 189 | 190 | pre code { 191 | display: block; 192 | box-shadow: none; 193 | } 194 | 195 | blockquote { 196 | color: #666; 197 | margin-bottom: 20px; 198 | padding: 0 0 0 20px; 199 | border-left: 3px solid #bbb; 200 | } 201 | 202 | ul, ol, dl { 203 | margin-bottom: 15px 204 | } 205 | 206 | ul li { 207 | list-style: inside; 208 | padding-left: 20px; 209 | } 210 | 211 | ol li { 212 | list-style: decimal inside; 213 | padding-left: 20px; 214 | } 215 | 216 | dl dt { 217 | font-weight: bold; 218 | } 219 | 220 | dl dd { 221 | padding-left: 20px; 222 | font-style: italic; 223 | } 224 | 225 | dl p { 226 | padding-left: 20px; 227 | font-style: italic; 228 | } 229 | 230 | hr { 231 | height: 1px; 232 | margin-bottom: 5px; 233 | border: none; 234 | background: url('../images/bg_hr.png') repeat-x center; 235 | } 236 | 237 | table { 238 | border: 1px solid #373737; 239 | margin-bottom: 20px; 240 | text-align: left; 241 | } 242 | 243 | th { 244 | font-family: 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif; 245 | padding: 10px; 246 | background: #373737; 247 | color: #fff; 248 | } 249 | 250 | td { 251 | padding: 10px; 252 | border: 1px solid #373737; 253 | } 254 | 255 | form { 256 | background: #f2f2f2; 257 | padding: 20px; 258 | } 259 | 260 | img { 261 | width: 100%; 262 | max-width: 100%; 263 | } 264 | 265 | /******************************************************************************* 266 | Full-Width Styles 267 | *******************************************************************************/ 268 | 269 | .outer { 270 | width: 100%; 271 | } 272 | 273 | .inner { 274 | position: relative; 275 | max-width: 640px; 276 | padding: 20px 10px; 277 | margin: 0 auto; 278 | } 279 | 280 | #forkme_banner { 281 | display: block; 282 | position: absolute; 283 | top:0; 284 | right: 10px; 285 | z-index: 10; 286 | padding: 10px 50px 10px 10px; 287 | color: #fff; 288 | background: url('../images/blacktocat.png') #0090ff no-repeat 95% 50%; 289 | font-weight: 700; 290 | box-shadow: 0 0 10px rgba(0,0,0,.5); 291 | border-bottom-left-radius: 2px; 292 | border-bottom-right-radius: 2px; 293 | } 294 | 295 | #header_wrap { 296 | background: #212121; 297 | background: -moz-linear-gradient(top, #373737, #212121); 298 | background: -webkit-linear-gradient(top, #373737, #212121); 299 | background: -ms-linear-gradient(top, #373737, #212121); 300 | background: -o-linear-gradient(top, #373737, #212121); 301 | background: linear-gradient(top, #373737, #212121); 302 | } 303 | 304 | #header_wrap .inner { 305 | padding: 50px 10px 30px 10px; 306 | } 307 | 308 | #project_title { 309 | margin: 0; 310 | color: #fff; 311 | font-size: 42px; 312 | font-weight: 700; 313 | text-shadow: #111 0px 0px 10px; 314 | } 315 | 316 | #project_tagline { 317 | color: #fff; 318 | font-size: 24px; 319 | font-weight: 300; 320 | background: none; 321 | text-shadow: #111 0px 0px 10px; 322 | } 323 | 324 | #downloads { 325 | position: absolute; 326 | width: 210px; 327 | z-index: 10; 328 | bottom: -40px; 329 | right: 0; 330 | height: 70px; 331 | background: url('../images/icon_download.png') no-repeat 0% 90%; 332 | } 333 | 334 | .zip_download_link { 335 | display: block; 336 | float: right; 337 | width: 90px; 338 | height:70px; 339 | text-indent: -5000px; 340 | overflow: hidden; 341 | background: url(../images/sprite_download.png) no-repeat bottom left; 342 | } 343 | 344 | .tar_download_link { 345 | display: block; 346 | float: right; 347 | width: 90px; 348 | height:70px; 349 | text-indent: -5000px; 350 | overflow: hidden; 351 | background: url(../images/sprite_download.png) no-repeat bottom right; 352 | margin-left: 10px; 353 | } 354 | 355 | .zip_download_link:hover { 356 | background: url(../images/sprite_download.png) no-repeat top left; 357 | } 358 | 359 | .tar_download_link:hover { 360 | background: url(../images/sprite_download.png) no-repeat top right; 361 | } 362 | 363 | #main_content_wrap { 364 | background: #f2f2f2; 365 | border-top: 1px solid #111; 366 | border-bottom: 1px solid #111; 367 | } 368 | 369 | #main_content { 370 | padding-top: 40px; 371 | } 372 | 373 | #footer_wrap { 374 | background: #212121; 375 | } 376 | 377 | 378 | 379 | /******************************************************************************* 380 | Small Device Styles 381 | *******************************************************************************/ 382 | 383 | @media screen and (max-width: 480px) { 384 | body { 385 | font-size:14px; 386 | } 387 | 388 | #downloads { 389 | display: none; 390 | } 391 | 392 | .inner { 393 | min-width: 320px; 394 | max-width: 480px; 395 | } 396 | 397 | #project_title { 398 | font-size: 32px; 399 | } 400 | 401 | h1 { 402 | font-size: 28px; 403 | } 404 | 405 | h2 { 406 | font-size: 24px; 407 | } 408 | 409 | h3 { 410 | font-size: 21px; 411 | } 412 | 413 | h4 { 414 | font-size: 18px; 415 | } 416 | 417 | h5 { 418 | font-size: 14px; 419 | } 420 | 421 | h6 { 422 | font-size: 12px; 423 | } 424 | 425 | code, pre { 426 | min-width: 320px; 427 | max-width: 480px; 428 | font-size: 11px; 429 | } 430 | 431 | } 432 | -------------------------------------------------------------------------------- /lib/consistent_hashing.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' if RUBY_VERSION !~ /^1\.9/ 2 | 3 | # Main module 4 | # 5 | module ConsistentHashing 6 | LIBPATH = ::File.expand_path('..', __FILE__) + ::File::SEPARATOR 7 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR 8 | VERSION = ::File.read(PATH + 'version.txt').strip 9 | 10 | # Internal: loads all necessary lib files 11 | # 12 | def self.load_lib 13 | require File.join(LIBPATH, 'consistent_hashing', 'virtual_point') 14 | require File.join(LIBPATH, 'consistent_hashing', 'ring') 15 | require File.join(LIBPATH, 'consistent_hashing', 'avl_tree') 16 | end 17 | end 18 | 19 | ConsistentHashing.load_lib 20 | -------------------------------------------------------------------------------- /lib/consistent_hashing/avl_tree.rb: -------------------------------------------------------------------------------- 1 | require 'avl_tree' 2 | 3 | module ConsistentHashing 4 | class AVLTree < ::AVLTree 5 | 6 | def minimum_pair() 7 | # Return the key with the smallest key value. 8 | return nil if @root.empty? 9 | 10 | current_node = @root 11 | while not current_node.left.empty? 12 | current_node = current_node.left 13 | end 14 | 15 | [current_node.key, current_node.value] 16 | end 17 | 18 | def next_gte_pair(key) 19 | # Returns the key/value pair with a key that follows the provided key in 20 | # sorted order. 21 | node = next_gte_node(@root, key) 22 | [node.key, node.value] if not node.empty? 23 | end 24 | 25 | protected 26 | 27 | def next_gte_node(node, key) 28 | return AVLTree::Node::EMPTY if node.empty? 29 | 30 | if key < node.key 31 | # The current key qualifies as after the provided key. However, we need 32 | # to check the tree on the left to see if there's a key in there also 33 | # greater than the provided key but less than the current key. 34 | after = next_gte_node(node.left, key) 35 | after = node if after.empty? 36 | elsif key > node.key 37 | # The current key will not be after the provided key, but something 38 | # in the right branch maybe. Check the right branch for the first key 39 | # larger than our value. 40 | after = next_gte_node(node.right, key) 41 | elsif node.key == key 42 | # An exact match qualifies as the next largest node. 43 | after = node 44 | end 45 | 46 | return after 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/consistent_hashing/ring.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | require 'set' 3 | 4 | module ConsistentHashing 5 | 6 | # Public: the hash ring containing all configured nodes 7 | # 8 | class Ring 9 | 10 | # Public: returns a new ring object 11 | def initialize(nodes = [], replicas = 3) 12 | @replicas = replicas 13 | @ring = AVLTree.new 14 | 15 | nodes.each { |node| add(node) } 16 | end 17 | 18 | # Public: returns the (virtual) points in the hash ring 19 | # 20 | # Returns: a Fixnum 21 | def length 22 | @ring.length 23 | end 24 | 25 | # Public: adds a new node into the hash ring 26 | # 27 | def add(node) 28 | @replicas.times do |i| 29 | # generate the key of this (virtual) point in the hash 30 | key = hash_key(node, i) 31 | 32 | @ring[key] = VirtualPoint.new(node, key) 33 | end 34 | end 35 | 36 | # Public: an alias for `add` 37 | # 38 | def <<(node) 39 | add(node) 40 | end 41 | 42 | # Public: removes a node from the hash ring 43 | # 44 | def delete(node) 45 | @replicas.times do |i| 46 | key = hash_key(node, i) 47 | 48 | @ring.delete key 49 | end 50 | end 51 | 52 | # Public: gets the point for an arbitrary key 53 | # 54 | # 55 | def point_for(key) 56 | return nil if @ring.empty? 57 | key = hash_key(key) 58 | _, value = @ring.next_gte_pair(key) 59 | _, value = @ring.minimum_pair unless value 60 | value 61 | end 62 | 63 | # Public: gets the node where to store the key 64 | # 65 | # Returns: the node Object 66 | def node_for(key) 67 | point_for(key).node 68 | end 69 | 70 | # Public: get all nodes in the ring 71 | # 72 | # Returns: an Array of the nodes in the ring 73 | def nodes 74 | nodes = points.map { |point| point.node } 75 | nodes.uniq 76 | end 77 | 78 | # Public: gets all points in the ring 79 | # 80 | # Returns: an Array of the points in the ring 81 | def points 82 | @ring.map { |point| point[1] } 83 | end 84 | 85 | protected 86 | 87 | # Internal: hashes the key 88 | # 89 | # Returns: a String 90 | def hash_key(key, index = nil) 91 | key = "#{key}:#{index}" if index 92 | Digest::MD5.hexdigest(key.to_s)[0..16].hex 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/consistent_hashing/virtual_point.rb: -------------------------------------------------------------------------------- 1 | module ConsistentHashing 2 | 3 | # Public: represents a virtual point on the hash ring 4 | # 5 | class VirtualPoint 6 | attr_reader :node, :index 7 | 8 | def initialize(node, index) 9 | @node = node 10 | @index = index.to_i 11 | end 12 | 13 | # Public: set a new index for the virtual point. Useful if the point gets duplicated 14 | def index=(index) 15 | @index = index.to_i 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /test/consistent_hashing/test_avl_tree.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w{ .. test_consistent_hashing}) 2 | 3 | class TestAVLTree < ConsistentHashing::TestCase 4 | AVLTree = ConsistentHashing::AVLTree 5 | 6 | def test_minimum_returns_nil_if_empty 7 | tree = AVLTree.new 8 | assert_nil tree.minimum_pair 9 | end 10 | 11 | def test_minimum_returns_root_node_if_only_node 12 | tree = AVLTree.new 13 | tree[0]= 1 14 | assert_equal(tree.minimum_pair, [0, 1]) 15 | end 16 | 17 | def test_minimum_returns_left_most_leaf 18 | tree = AVLTree.new 19 | 1.upto(10) do |i| 20 | tree[i] = i.to_s 21 | end 22 | 23 | assert_equal(tree.minimum_pair, [1, "1"]) 24 | end 25 | 26 | def test_next_gte_pair_returns_nil_if_empty 27 | tree = AVLTree.new 28 | assert_nil tree.next_gte_pair("some key") 29 | end 30 | 31 | def test_next_gte_pair_finds_exact_match_key 32 | tree = AVLTree.new 33 | 1.upto(10) do |i| 34 | tree[i] = i.to_s 35 | end 36 | 37 | 1.upto(10) do |i| 38 | assert_equal(tree.next_gte_pair(i), [i, i.to_s]) 39 | end 40 | end 41 | 42 | def test_next_gte_pair_finds_slightly_larger_key 43 | tree = AVLTree.new 44 | (2..11).step(2) do |i| 45 | tree[i] = i.to_s 46 | end 47 | 48 | (1..10).step(2) do |i| 49 | assert_equal(tree.next_gte_pair(i), [i + 1, (i + 1).to_s]) 50 | end 51 | end 52 | 53 | def test_next_gte_pair_returns_nil_if_no_larger_keys 54 | tree = AVLTree.new 55 | (2..11).step(2) do |i| 56 | tree[i] = i.to_s 57 | end 58 | 59 | assert_nil tree.next_gte_pair(12) 60 | end 61 | end 62 | 63 | -------------------------------------------------------------------------------- /test/consistent_hashing/test_ring.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w{ .. test_consistent_hashing}) 2 | 3 | class TestRing < ConsistentHashing::TestCase 4 | def setup 5 | @ring = ConsistentHashing::Ring.new %w{A B C} 6 | 7 | @examples = { 8 | "A" => "foobar", 9 | "B" => "123", 10 | "C" => "baz", 11 | "not_found" => 0 12 | } 13 | end 14 | 15 | def test_init 16 | ring = ConsistentHashing::Ring.new 17 | assert_equal 0, ring.length 18 | end 19 | 20 | def test_length 21 | ring = ConsistentHashing::Ring.new([], 3) 22 | assert_equal 0, ring.length 23 | 24 | ring << "A" 25 | ring << "B" 26 | assert_equal 6, ring.length 27 | end 28 | 29 | # builds a rather huge ring of 1024 nodes and 10 replicas each 30 | # 31 | def test_add_huge 32 | replicas = 10 33 | ring = ConsistentHashing::Ring.new([], replicas) 34 | assert_equal 0, ring.length 35 | 36 | (1..1024).map {|i| ring.add(i)} 37 | 38 | assert_equal 10240, ring.length 39 | end 40 | 41 | def test_get_node 42 | assert_equal "A", @ring.point_for(@examples["A"]).node 43 | assert_equal "B", @ring.point_for(@examples["B"]).node 44 | assert_equal "C", @ring.point_for(@examples["C"]).node 45 | end 46 | 47 | # should fall back to the first node, if key > last node 48 | def test_get_node_fallback_to_first 49 | ring = ConsistentHashing::Ring.new ["A"], 1 50 | 51 | point = ring.point_for(@examples["not_found"]) 52 | 53 | assert_equal "A", point.node 54 | assert_not_equal 0, point.index 55 | end 56 | 57 | # if I remove node C, all keys previously mapped to C should be moved clockwise to 58 | # the next node. That's a virtual point of B here 59 | def test_remove_node 60 | @ring.delete("C") 61 | assert_equal "B", @ring.point_for(@examples["C"]).node 62 | end 63 | 64 | def test_point_for 65 | assert_equal "C", @ring.node_for(@examples["C"]) 66 | end 67 | 68 | def test_nodes 69 | nodes = @ring.nodes 70 | 71 | assert_equal 3, nodes.length 72 | assert_not_equal nil, nodes.index("A") 73 | assert_not_equal nil, nodes.index("B") 74 | assert_not_equal nil, nodes.index("C") 75 | end 76 | 77 | def test_points 78 | ring = ConsistentHashing::Ring.new %w{A B C}, 3 79 | 80 | points = ring.points 81 | assert_equal 9, points.length 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/consistent_hashing/test_virtual_point.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w{ .. test_consistent_hashing}) 2 | 3 | class TestVirtualPoint < ConsistentHashing::TestCase 4 | def setup 5 | @node = {'host' => '192.168.1.101'} 6 | @point = ConsistentHashing::VirtualPoint.new(@node, "1") 7 | end 8 | 9 | def test_init 10 | assert_equal @node, @point.node 11 | assert_equal 1, @point.index 12 | end 13 | 14 | def test_set_index 15 | @point.index = "2" 16 | assert_equal 2, @point.index 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/test_consistent_hashing.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.join(File.dirname(__FILE__), %w{.. lib})) 2 | 3 | begin 4 | require 'simplecov' 5 | SimpleCov.start 6 | rescue LoadError 7 | # ignore 8 | end 9 | 10 | require 'consistent_hashing' 11 | require 'test/unit' 12 | 13 | module ConsistentHashing 14 | class TestCase < Test::Unit::TestCase 15 | def test_module 16 | assert_not_nil ConsistentHashing::VERSION 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 2.0.0 --------------------------------------------------------------------------------