├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config └── config.exs ├── lib └── rendezvous.ex ├── mix.exs ├── mix.lock └── test ├── rendezvous_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | otp_release: 4 | - 17.4 5 | - 17.3 6 | - 17.1 7 | 8 | elixir: 9 | - 1.0.4 10 | - 1.0.3 11 | - 1.0.2 12 | - 1.0.1 13 | - 1.0.0 14 | 15 | before_script: 16 | - git clone https://github.com/jeremyjh/dialyxir.git 17 | - cd dialyxir 18 | - mix archive.build 19 | - mix archive.install --force 20 | - mix dialyzer.plt 21 | - cd .. 22 | 23 | script: 24 | - mix compile 25 | - mix test 26 | - mix dialyzer 27 | 28 | after_success: 29 | - mix compile 30 | - mix coveralls.travis 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tim de Putter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rendezvous [![Build Status](https://travis-ci.org/timdeputter/Rendezvous.svg?branch=master)](https://travis-ci.org/timdeputter/Rendezvous) [![Hex.pm package version](https://img.shields.io/hexpm/v/Rendezvous.svg?style=flat)](https://hex.pm/packages/Rendezvous) [![Coverage Status](https://coveralls.io/repos/Puddah/Rendezvous/badge.svg?branch=master)](https://coveralls.io/r/Puddah/Rendezvous?branch=master) [![License](http://img.shields.io/hexpm/l/Rendezvous.svg?style=flat)](https://github.com/Puddah/Rendezvous/blob/master/LICENSE) 2 | ========== 3 | 4 | Implementation of the [Rendezvous or Highest Random Weight (HRW) hashing algorithm](https://en.wikipedia.org/wiki/Rendezvous_hashing) in the Elixir Programming Language ([elixir-lang.org](http://elixir-lang.org)) 5 | 6 | 7 | 8 | ## Installation 9 | 10 | Add rendezvous as a dependency in your mix.exs file: 11 | 12 | ```elixir 13 | defp deps do 14 | [ 15 | rendezvous: "~> 0.0.1" 16 | ] 17 | end 18 | ``` 19 | 20 | and run `mix deps.get`. 21 | 22 | ## Usage 23 | 24 | Use Rendezvous.get to obtain for an object the corresponding bucket from a list of buckets. The first parameter defines the hashing algorithm to use. 25 | 26 | ```elixir 27 | object = "Some object to put in bucket" 28 | buckets = ["bucket A", "bucket B", "bucket C"] 29 | Rendezvous.get(:sha, object, buckets) 30 | ``` 31 | 32 | or use Rendezvous.get_node to obtain a nodename from the nodes in an elixir cluster: 33 | 34 | ```elixir 35 | Rendezvous.get_node object # returns name of elixir node 36 | ``` 37 | 38 | ## License 39 | 40 | Check [LICENSE](LICENSE) file for more information. 41 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /lib/rendezvous.ex: -------------------------------------------------------------------------------- 1 | defmodule Rendezvous do 2 | import Enum 3 | use FitEx 4 | 5 | @spec get_node(binary) :: binary 6 | def get_node key do 7 | nodes = map [Node.self | Node.list], &to_string/1 8 | get(:sha, key, nodes) 9 | end 10 | 11 | @spec get(atom, binary, [binary]) :: binary 12 | def get algorithm, key, buckets do 13 | {bucket, _hash} = map(buckets, f get_bucket_with_hash(algorithm, it, key)) 14 | |> max_by fn {_b, hash} -> hash end 15 | bucket 16 | end 17 | 18 | defp get_bucket_with_hash(algorithm, bucket, key) do 19 | {bucket, compute_bucket_hash(algorithm, bucket, key)} 20 | end 21 | 22 | defp compute_bucket_hash(algorithm, bucket,key) do 23 | :crypto.hash(algorithm,[bucket,key] ) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Rendezvous.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :rendezvous, 6 | version: "0.0.1", 7 | description: "Implementation of the Rendezvous or Highest Random Weight (HRW) hashing algorithm", 8 | elixir: "~> 1.0", 9 | package: package, 10 | deps: deps, 11 | test_coverage: [tool: ExCoveralls] 12 | ] 13 | end 14 | 15 | defp deps do 16 | [{:excoveralls, "~> 0.3", only: [:dev, :test]},{:fitex, "~> 0.0.1"}] 17 | end 18 | 19 | defp package do 20 | [files: ["lib", "mix.exs", "README*", "readme*", "LICENSE*", "license*"], 21 | contributors: ["Tim de Putter"], 22 | licenses: ["The MIT License"], 23 | links: %{"GitHub" => "https://github.com/Puddah/Rendezvous"}] 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"excoveralls": {:hex, :excoveralls, "0.3.10"}, 2 | "exjsx": {:hex, :exjsx, "3.1.0"}, 3 | "fitex": {:hex, :fitex, "0.0.1"}, 4 | "hackney": {:hex, :hackney, "1.1.0"}, 5 | "idna": {:hex, :idna, "1.0.2"}, 6 | "jsx": {:hex, :jsx, "2.4.0"}, 7 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.4"}} 8 | -------------------------------------------------------------------------------- /test/rendezvous_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RendezvousTest do 2 | use ExUnit.Case 3 | import Enum 4 | import Rendezvous 5 | import List, only: [delete: 2] 6 | 7 | setup do 8 | buckets = ["Node A", "Node B", "Node C"] 9 | {:ok, buckets: buckets} 10 | end 11 | 12 | test "Determines for a given key the appropriate bucket", context do 13 | bucket = get_hrwbucket("some key", context[:buckets]) 14 | assert is_bucket(context[:buckets], bucket) 15 | end 16 | 17 | test "Allways uses the same bucket for the same object", context do 18 | buckets = get_buckets(["key","key","key"], context[:buckets]) 19 | assert all_equal?(buckets) 20 | end 21 | 22 | test "Distributes objects evenly on buckets", context do 23 | buckets_for_objects = get_buckets(generate_random_values, context[:buckets]) 24 | assert contains_elements_in_equal_sized_parts(buckets_for_objects, context[:buckets]) 25 | end 26 | 27 | test "When a buckets fails only the objects in that bucket have to be moved", context do 28 | bucketsWithoutB = delete(context[:buckets], "Node B") 29 | assert get_hrwbucket("A",bucketsWithoutB) == get_hrwbucket("A",context[:buckets]) 30 | assert get_hrwbucket("AAAF",bucketsWithoutB) == get_hrwbucket("AAAF",context[:buckets]) 31 | assert get_hrwbucket("AA",bucketsWithoutB) != get_hrwbucket("AA",context[:buckets]) 32 | assert member? bucketsWithoutB, get_hrwbucket("AA",bucketsWithoutB) 33 | end 34 | 35 | test "Determines for a given key the appropriate node in an elixir cluster" do 36 | node = get_node "A" 37 | assert to_string Node.self == node 38 | end 39 | 40 | defp contains_elements_in_equal_sized_parts buckets_for_objects, [current_bucket | rest] do 41 | between(get_percentage_of(buckets_for_objects, current_bucket), 32, 34) 42 | && contains_elements_in_equal_sized_parts(buckets_for_objects,rest) 43 | end 44 | 45 | defp contains_elements_in_equal_sized_parts _, [] do 46 | true 47 | end 48 | 49 | defp between value, lower, upper do 50 | lower <= value && value <= upper 51 | end 52 | 53 | defp generate_random_values do 54 | generate_randoms(12000) 55 | end 56 | 57 | defp generate_randoms(1) do 58 | [generate_random_number()] 59 | end 60 | 61 | defp generate_randoms(count) do 62 | [generate_random_number()] ++ generate_randoms(count-1) 63 | end 64 | 65 | defp generate_random_number do 66 | to_string(:random.uniform) 67 | end 68 | 69 | defp get_percentage_of(collection, object) do 70 | number_of_eq_to_object = filter(collection, fn(e) -> e == object end) |> count 71 | result = Float.round(100 / count(collection) * number_of_eq_to_object,0) 72 | result 73 | end 74 | 75 | defp get_buckets(objects,buckets) do 76 | map(objects, fn(object) -> get_hrwbucket(object, buckets) end) 77 | end 78 | 79 | defp get_hrwbucket(object,buckets) do 80 | get(:sha, object, buckets) 81 | end 82 | 83 | defp is_bucket(buckets, may_be_bucket) do 84 | any?(buckets, fn e -> e == may_be_bucket end) 85 | end 86 | 87 | defp all_equal?(collection) do 88 | uniq(collection) |> count == 1 89 | end 90 | 91 | end 92 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------