The project is in a healthy, maintained state
Consistent hash ring with virtual nodes, weighted members, and replication support. Minimal key redistribution when nodes are added or removed.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-hash_ring

Tests Gem Version Last updated

Consistent hashing for distributed key distribution

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem 'philiprehberger-hash_ring'

Or install directly:

gem install philiprehberger-hash_ring

Usage

require 'philiprehberger/hash_ring'

ring = Philiprehberger::HashRing::Ring.new(['cache-1', 'cache-2', 'cache-3'])

ring.get('user:42')       # => "cache-2"
ring.get('session:abc')   # => "cache-1"

Weighted Nodes

Assign higher weight to nodes with more capacity:

ring = Philiprehberger::HashRing::Ring.new
ring.add('small-server', weight: 1)
ring.add('large-server', weight: 3)

Replication

Fetch multiple distinct nodes for redundancy:

ring.get_n('user:42', 2)  # => ["cache-2", "cache-3"]

Custom Hash Function

Use a different hash algorithm instead of the default MD5:

ring = Philiprehberger::HashRing::Ring.new(
  ['cache-1', 'cache-2'],
  hash: ->(key) { Digest::SHA256.hexdigest(key) }
)

Migration Plan

Compare two ring topologies to plan node changes:

old_ring = Philiprehberger::HashRing::Ring.new(['node-a', 'node-b', 'node-c'])
new_ring = Philiprehberger::HashRing::Ring.new(['node-a', 'node-b', 'node-c', 'node-d'])

plan = old_ring.migration_plan(new_ring)
plan[:moved]    # => [{key_sample: "key_42", from: "node-b", to: "node-d"}, ...]
plan[:summary]  # => {"node-d" => {gained: 2480, lost: 0}, ...}

Serialization

Save and restore ring state as JSON:

json = ring.to_json
restored = Philiprehberger::HashRing::Ring.from_json(json)

Balance Score

Measure how evenly keys are distributed across nodes:

ring.balance_score  # => 0.95 (1.0 = perfectly balanced)

Batch Key Routing

Find which node handles each key in a batch:

result = ring.nodes_for_keys(['user:1', 'user:2', 'user:3'])
# => {"cache-1" => ["user:1", "user:3"], "cache-2" => ["user:2"]}

Distribution Analysis

Check how keys are spread across nodes:

keys = (0...1000).map { |i| "key-#{i}" }
ring.distribution(keys)   # => {"cache-1"=>312, "cache-2"=>355, "cache-3"=>333}

Adding and Removing Nodes

ring.add('cache-4')       # Only a fraction of keys are redistributed
ring.remove('cache-1')    # Remaining nodes absorb the removed node's keys

API

Method Description
Ring.new(nodes = [], replicas: 150, hash: nil) Create a ring with optional custom hash function
Ring.from_json(data) Reconstruct a ring from JSON string
ring.add(node, weight: 1) Add a node (weight multiplies replicas)
ring.remove(node) Remove a node and its virtual nodes
ring.get(key) Get the node responsible for a key
ring.get_n(key, n) Get n distinct physical nodes for a key
ring.nodes List all physical nodes
ring.size Number of physical nodes
ring.empty? Check if the ring is empty
ring.distribution(keys) Hash of {node => count} showing key distribution
ring.migration_plan(other_ring) Compare topologies and show key movement
ring.to_json Serialize ring state to JSON
ring.balance_score Distribution quality score (0.0-1.0)
ring.nodes_for_keys(keys) Map each key to its responsible node

Development

bundle install
bundle exec rspec      # Run tests
bundle exec rubocop    # Check code style

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT