├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── custom_hasher.rs └── readme.rs └── src ├── hash_ring.rs └── lib.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | 7 | ################################################### 8 | # Main Builds 9 | ################################################### 10 | 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | rust: [stable, beta, nightly] 18 | os: [ubuntu-latest, windows-latest, macOS-latest] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | 24 | - name: Install rust 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: ${{ matrix.rust }} 28 | profile: minimal 29 | override: true 30 | 31 | - uses: davidB/rust-cargo-make@v1 32 | with: 33 | version: '0.32.9' 34 | 35 | - name: Build and run tests 36 | env: 37 | CARGO_MAKE_RUN_CODECOV: true 38 | run: | 39 | cargo make --no-workspace workspace-ci-flow 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.2.0] - 2020-12-13 10 | ### Added 11 | - CHANGELOG.md 12 | - Support for switching out the hash function. 13 | 14 | ### Changed 15 | - get_node function to take '&self' instead of '&mut self'. 16 | - Moved from TravisCI and AppVeyor to Github Actions for CI. 17 | - Moved from Coveralls to CodeCov for code coverage tracking. 18 | - Hashing function changed to xxHash64 from md5. 19 | 20 | ### Fixed 21 | - A bug that deleted the first node in the ring when trying to delete a node that didn't exist. 22 | 23 | [Unreleased]: https://github.com/mattnenterprise/rust-hash-ring/compare/v0.2.0...HEAD 24 | [0.2.0]: https://github.com/mattnenterprise/rust-hash-ring/compare/v0.1.7...v0.2.0 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "hash_ring" 4 | version = "0.2.0" 5 | authors = ["Matt McCoy "] 6 | documentation = "https://docs.rs/hash_ring" 7 | repository = "https://github.com/mattnenterprise/rust-hash-ring" 8 | homepage = "https://github.com/mattnenterprise/rust-hash-ring" 9 | description = "Consistent Hashing library for Rust" 10 | readme = "README.md" 11 | license = "MIT" 12 | 13 | keywords = ["consistent-hashing", "hash-ring"] 14 | categories = ["data-structures"] 15 | 16 | [dependencies] 17 | twox-hash = "1.6.0" 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Matt McCoy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rust-hash-ring 2 | ================ 3 | 4 | Consistent Hashing library for Rust 5 | 6 | [![Crates.io](https://img.shields.io/crates/d/hash_ring.svg)](https://crates.io/crates/hash_ring) 7 | [![crates.io](https://img.shields.io/crates/v/hash_ring.svg)](https://crates.io/crates/hash_ring) 8 | [![Crates.io](https://img.shields.io/crates/l/hash_ring.svg)](https://crates.io/crates/hash_ring) 9 | [![CI](https://github.com/mattnenterprise/rust-hash-ring/workflows/CI/badge.svg)](https://github.com/mattnenterprise/rust-hash-ring/actions?query=workflow%3ACI) 10 | [![Coverage Status](https://codecov.io/gh/mattnenterprise/rust-hash-ring/branch/master/graph/badge.svg)](https://app.codecov.io/gh/mattnenterprise/rust-hash-ring/branch/master) 11 | 12 | [Documentation](https://docs.rs/hash_ring) 13 | 14 | ### Usage 15 | ```rust 16 | extern crate hash_ring; 17 | 18 | use hash_ring::HashRing; 19 | use hash_ring::NodeInfo; 20 | 21 | fn main() { 22 | let mut nodes: Vec = Vec::new(); 23 | nodes.push(NodeInfo { 24 | host: "localhost", 25 | port: 15324, 26 | }); 27 | nodes.push(NodeInfo { 28 | host: "localhost", 29 | port: 15325, 30 | }); 31 | nodes.push(NodeInfo { 32 | host: "localhost", 33 | port: 15326, 34 | }); 35 | nodes.push(NodeInfo { 36 | host: "localhost", 37 | port: 15327, 38 | }); 39 | nodes.push(NodeInfo { 40 | host: "localhost", 41 | port: 15328, 42 | }); 43 | nodes.push(NodeInfo { 44 | host: "localhost", 45 | port: 15329, 46 | }); 47 | 48 | let mut hash_ring: HashRing = HashRing::new(nodes, 10); 49 | 50 | println!( 51 | "Key: '{}', Node: {}", 52 | "hello", 53 | hash_ring.get_node(("hello").to_string()).unwrap() 54 | ); 55 | 56 | println!( 57 | "Key: '{}', Node: {}", 58 | "dude", 59 | hash_ring.get_node(("dude").to_string()).unwrap() 60 | ); 61 | 62 | println!( 63 | "Key: '{}', Node: {}", 64 | "martian", 65 | hash_ring.get_node(("martian").to_string()).unwrap() 66 | ); 67 | 68 | println!( 69 | "Key: '{}', Node: {}", 70 | "tardis", 71 | hash_ring.get_node(("tardis").to_string()).unwrap() 72 | ); 73 | 74 | hash_ring.remove_node(&NodeInfo { 75 | host: "localhost", 76 | port: 15329, 77 | }); 78 | 79 | println!( 80 | "Key: '{}', Node: {}", 81 | "hello", 82 | hash_ring.get_node(("hello").to_string()).unwrap() 83 | ); 84 | 85 | hash_ring.add_node(&NodeInfo { 86 | host: "localhost", 87 | port: 15329, 88 | }); 89 | 90 | println!( 91 | "Key: '{}', Node: {}", 92 | "hello", 93 | hash_ring.get_node(("hello").to_string()).unwrap() 94 | ); 95 | } 96 | ``` 97 | 98 | For an example of how to use a custom hash function you can look at [examples/custom_hasher.rs](https://github.com/mattnenterprise/rust-hash-ring/blob/master/examples/custom_hasher.rs) 99 | 100 | ### Contributing 101 | Just fork it, implement your changes and submit a pull request. 102 | 103 | ### License 104 | 105 | MIT 106 | -------------------------------------------------------------------------------- /examples/custom_hasher.rs: -------------------------------------------------------------------------------- 1 | extern crate hash_ring; 2 | 3 | use hash_ring::HashRing; 4 | use hash_ring::NodeInfo; 5 | use std::hash::BuildHasherDefault; 6 | use std::hash::Hasher; 7 | 8 | // This is a hasher that always returns the same number 9 | // no matter the input. This is meant as an example and 10 | // should never be used in production code as all keys go 11 | // to the same node. 12 | #[derive(Default)] 13 | struct ConstantHasher; 14 | 15 | impl Hasher for ConstantHasher { 16 | fn write(&mut self, _bytes: &[u8]) { 17 | // Do nothing 18 | } 19 | 20 | fn finish(&self) -> u64 { 21 | return 1; 22 | } 23 | } 24 | 25 | type ConstantBuildHasher = BuildHasherDefault; 26 | 27 | fn main() { 28 | let mut nodes: Vec = Vec::new(); 29 | nodes.push(NodeInfo { 30 | host: "localhost", 31 | port: 15324, 32 | }); 33 | nodes.push(NodeInfo { 34 | host: "localhost", 35 | port: 15325, 36 | }); 37 | nodes.push(NodeInfo { 38 | host: "localhost", 39 | port: 15326, 40 | }); 41 | nodes.push(NodeInfo { 42 | host: "localhost", 43 | port: 15327, 44 | }); 45 | nodes.push(NodeInfo { 46 | host: "localhost", 47 | port: 15328, 48 | }); 49 | nodes.push(NodeInfo { 50 | host: "localhost", 51 | port: 15329, 52 | }); 53 | 54 | let hash_ring: HashRing = 55 | HashRing::with_hasher(nodes, 10, ConstantBuildHasher::default()); 56 | 57 | println!( 58 | "Key: '{}', Node: {}", 59 | "hello", 60 | hash_ring.get_node(("hello").to_string()).unwrap() 61 | ); 62 | println!( 63 | "Key: '{}', Node: {}", 64 | "dude", 65 | hash_ring.get_node(("dude").to_string()).unwrap() 66 | ); 67 | println!( 68 | "Key: '{}', Node: {}", 69 | "martian", 70 | hash_ring.get_node(("martian").to_string()).unwrap() 71 | ); 72 | println!( 73 | "Key: '{}', Node: {}", 74 | "tardis", 75 | hash_ring.get_node(("tardis").to_string()).unwrap() 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /examples/readme.rs: -------------------------------------------------------------------------------- 1 | extern crate hash_ring; 2 | 3 | use hash_ring::HashRing; 4 | use hash_ring::NodeInfo; 5 | 6 | fn main() { 7 | let mut nodes: Vec = Vec::new(); 8 | nodes.push(NodeInfo { 9 | host: "localhost", 10 | port: 15324, 11 | }); 12 | nodes.push(NodeInfo { 13 | host: "localhost", 14 | port: 15325, 15 | }); 16 | nodes.push(NodeInfo { 17 | host: "localhost", 18 | port: 15326, 19 | }); 20 | nodes.push(NodeInfo { 21 | host: "localhost", 22 | port: 15327, 23 | }); 24 | nodes.push(NodeInfo { 25 | host: "localhost", 26 | port: 15328, 27 | }); 28 | nodes.push(NodeInfo { 29 | host: "localhost", 30 | port: 15329, 31 | }); 32 | 33 | let mut hash_ring: HashRing = HashRing::new(nodes, 10); 34 | 35 | println!( 36 | "Key: '{}', Node: {}", 37 | "hello", 38 | hash_ring.get_node(("hello").to_string()).unwrap() 39 | ); 40 | 41 | println!( 42 | "Key: '{}', Node: {}", 43 | "dude", 44 | hash_ring.get_node(("dude").to_string()).unwrap() 45 | ); 46 | 47 | println!( 48 | "Key: '{}', Node: {}", 49 | "martian", 50 | hash_ring.get_node(("martian").to_string()).unwrap() 51 | ); 52 | 53 | println!( 54 | "Key: '{}', Node: {}", 55 | "tardis", 56 | hash_ring.get_node(("tardis").to_string()).unwrap() 57 | ); 58 | 59 | hash_ring.remove_node(&NodeInfo { 60 | host: "localhost", 61 | port: 15329, 62 | }); 63 | 64 | println!( 65 | "Key: '{}', Node: {}", 66 | "hello", 67 | hash_ring.get_node(("hello").to_string()).unwrap() 68 | ); 69 | 70 | hash_ring.add_node(&NodeInfo { 71 | host: "localhost", 72 | port: 15329, 73 | }); 74 | 75 | println!( 76 | "Key: '{}', Node: {}", 77 | "hello", 78 | hash_ring.get_node(("hello").to_string()).unwrap() 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/hash_ring.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BinaryHeap; 2 | use std::collections::HashMap; 3 | use std::fmt::{self}; 4 | use std::hash::BuildHasher; 5 | use std::hash::BuildHasherDefault; 6 | use std::hash::Hasher; 7 | use twox_hash::XxHash64; 8 | 9 | /// As a convenience, rust-hash-ring provides a default struct to hold node 10 | /// information. It is optional and you can define your own. 11 | #[derive(Clone, Debug, PartialEq)] 12 | pub struct NodeInfo { 13 | pub host: &'static str, 14 | pub port: u16, 15 | } 16 | 17 | impl fmt::Display for NodeInfo { 18 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | write!(fmt, "{}:{}", self.host, self.port) 20 | } 21 | } 22 | 23 | type XxHash64Hasher = BuildHasherDefault; 24 | 25 | /// HashRing 26 | pub struct HashRing { 27 | replicas: isize, 28 | ring: HashMap, 29 | sorted_keys: Vec, 30 | hash_builder: S, 31 | } 32 | 33 | impl HashRing { 34 | /// Creates a new hash ring with the specified nodes. 35 | /// Replicas is the number of virtual nodes each node has to make a better distribution. 36 | pub fn new(nodes: Vec, replicas: isize) -> HashRing { 37 | HashRing::with_hasher(nodes, replicas, XxHash64Hasher::default()) 38 | } 39 | } 40 | 41 | impl HashRing 42 | where 43 | T: ToString + Clone, 44 | S: BuildHasher, 45 | { 46 | pub fn with_hasher(nodes: Vec, replicas: isize, hash_builder: S) -> HashRing { 47 | let mut new_hash_ring: HashRing = HashRing { 48 | replicas, 49 | ring: HashMap::new(), 50 | sorted_keys: Vec::new(), 51 | hash_builder, 52 | }; 53 | 54 | for n in &nodes { 55 | new_hash_ring.add_node(n); 56 | } 57 | new_hash_ring 58 | } 59 | 60 | /// Adds a node to the hash ring 61 | pub fn add_node(&mut self, node: &T) { 62 | for i in 0..self.replicas { 63 | let key = self.gen_key(format!("{}:{}", node.to_string(), i)); 64 | self.ring.insert(key, (*node).clone()); 65 | self.sorted_keys.push(key); 66 | } 67 | 68 | self.sorted_keys = BinaryHeap::from(self.sorted_keys.clone()).into_sorted_vec(); 69 | } 70 | 71 | /// Deletes a node from the hash ring 72 | pub fn remove_node(&mut self, node: &T) { 73 | for i in 0..self.replicas { 74 | let key = self.gen_key(format!("{}:{}", node.to_string(), i)); 75 | if !self.ring.contains_key(&key) { 76 | return; 77 | } 78 | self.ring.remove(&key); 79 | let mut index = 0; 80 | for j in 0..self.sorted_keys.len() { 81 | if self.sorted_keys[j] == key { 82 | index = j; 83 | break; 84 | } 85 | } 86 | self.sorted_keys.remove(index); 87 | } 88 | } 89 | 90 | /// Gets the node a specific key belongs to 91 | pub fn get_node(&self, key: String) -> Option<&T> { 92 | if self.sorted_keys.is_empty() { 93 | return None; 94 | } 95 | 96 | let generated_key = self.gen_key(key); 97 | let nodes = self.sorted_keys.clone(); 98 | 99 | for node in &nodes { 100 | if generated_key <= *node { 101 | return Some(self.ring.get(node).unwrap()); 102 | } 103 | } 104 | 105 | let node = &nodes[0]; 106 | return Some(self.ring.get(node).unwrap()); 107 | } 108 | 109 | /// Generates a key from a string value 110 | fn gen_key(&self, key: String) -> u64 { 111 | let mut hasher = self.hash_builder.build_hasher(); 112 | hasher.write(key.as_bytes()); 113 | hasher.finish() 114 | } 115 | } 116 | 117 | #[cfg(test)] 118 | mod test { 119 | use hash_ring::{HashRing, NodeInfo}; 120 | use std::hash::BuildHasherDefault; 121 | use std::hash::Hasher; 122 | 123 | // Defines a NodeInfo for a localhost address with a given port. 124 | fn node(port: u16) -> NodeInfo { 125 | NodeInfo { 126 | host: "localhost", 127 | port, 128 | } 129 | } 130 | 131 | #[test] 132 | fn test_empty_ring() { 133 | let hash_ring: HashRing = HashRing::new(vec![], 10); 134 | assert_eq!(None, hash_ring.get_node("hello".to_string())); 135 | } 136 | 137 | #[test] 138 | fn test_default_nodes() { 139 | let mut nodes: Vec = Vec::new(); 140 | nodes.push(node(15324)); 141 | nodes.push(node(15325)); 142 | nodes.push(node(15326)); 143 | nodes.push(node(15327)); 144 | nodes.push(node(15328)); 145 | nodes.push(node(15329)); 146 | 147 | let mut hash_ring: HashRing = HashRing::new(nodes, 10); 148 | 149 | assert_eq!(Some(&node(15324)), hash_ring.get_node("two".to_string())); 150 | assert_eq!(Some(&node(15325)), hash_ring.get_node("seven".to_string())); 151 | assert_eq!(Some(&node(15326)), hash_ring.get_node("hello".to_string())); 152 | assert_eq!(Some(&node(15327)), hash_ring.get_node("dude".to_string())); 153 | assert_eq!(Some(&node(15328)), hash_ring.get_node("fourteen".to_string())); 154 | assert_eq!(Some(&node(15329)), hash_ring.get_node("five".to_string())); 155 | 156 | hash_ring.remove_node(&node(15329)); 157 | assert_eq!(Some(&node(15326)), hash_ring.get_node("hello".to_string())); 158 | 159 | hash_ring.add_node(&node(15329)); 160 | assert_eq!(Some(&node(15326)), hash_ring.get_node("hello".to_string())); 161 | } 162 | 163 | #[derive(Clone)] 164 | struct CustomNodeInfo { 165 | pub host: &'static str, 166 | pub port: u16, 167 | } 168 | 169 | impl ToString for CustomNodeInfo { 170 | fn to_string(&self) -> String { 171 | format!("{}:{}", self.host, self.port) 172 | } 173 | } 174 | 175 | #[test] 176 | fn test_custom_nodes() { 177 | let mut nodes: Vec = Vec::new(); 178 | nodes.push(CustomNodeInfo { 179 | host: "localhost", 180 | port: 15324, 181 | }); 182 | nodes.push(CustomNodeInfo { 183 | host: "localhost", 184 | port: 15325, 185 | }); 186 | nodes.push(CustomNodeInfo { 187 | host: "localhost", 188 | port: 15326, 189 | }); 190 | nodes.push(CustomNodeInfo { 191 | host: "localhost", 192 | port: 15327, 193 | }); 194 | nodes.push(CustomNodeInfo { 195 | host: "localhost", 196 | port: 15328, 197 | }); 198 | nodes.push(CustomNodeInfo { 199 | host: "localhost", 200 | port: 15329, 201 | }); 202 | 203 | let mut hash_ring: HashRing = HashRing::new(nodes, 10); 204 | 205 | assert_eq!( 206 | Some("localhost:15326".to_string()), 207 | hash_ring 208 | .get_node("hello".to_string()) 209 | .map(|x| x.to_string(),) 210 | ); 211 | assert_eq!( 212 | Some("localhost:15327".to_string()), 213 | hash_ring 214 | .get_node("dude".to_string()) 215 | .map(|x| x.to_string(),) 216 | ); 217 | 218 | hash_ring.remove_node(&CustomNodeInfo { 219 | host: "localhost", 220 | port: 15329, 221 | }); 222 | assert_eq!( 223 | Some("localhost:15326".to_string()), 224 | hash_ring 225 | .get_node("hello".to_string()) 226 | .map(|x| x.to_string(),) 227 | ); 228 | 229 | hash_ring.add_node(&CustomNodeInfo { 230 | host: "localhost", 231 | port: 15329, 232 | }); 233 | assert_eq!( 234 | Some("localhost:15326".to_string()), 235 | hash_ring 236 | .get_node("hello".to_string()) 237 | .map(|x| x.to_string(),) 238 | ); 239 | } 240 | 241 | #[test] 242 | fn test_remove_actual_node() { 243 | let mut nodes: Vec = Vec::new(); 244 | nodes.push(node(15324)); 245 | nodes.push(node(15325)); 246 | nodes.push(node(15326)); 247 | nodes.push(node(15327)); 248 | nodes.push(node(15328)); 249 | nodes.push(node(15329)); 250 | 251 | let mut hash_ring: HashRing = HashRing::new(nodes, 10); 252 | 253 | // This should be num nodes * num replicas 254 | assert_eq!(60, hash_ring.sorted_keys.len()); 255 | assert_eq!(60, hash_ring.ring.len()); 256 | 257 | hash_ring.remove_node(&node(15326)); 258 | 259 | // This should be num nodes * num replicas 260 | assert_eq!(50, hash_ring.sorted_keys.len()); 261 | assert_eq!(50, hash_ring.ring.len()); 262 | } 263 | 264 | #[test] 265 | fn test_remove_non_existent_node() { 266 | let mut nodes: Vec = Vec::new(); 267 | nodes.push(node(15324)); 268 | nodes.push(node(15325)); 269 | nodes.push(node(15326)); 270 | nodes.push(node(15327)); 271 | nodes.push(node(15328)); 272 | nodes.push(node(15329)); 273 | 274 | let mut hash_ring: HashRing = HashRing::new(nodes, 10); 275 | 276 | hash_ring.remove_node(&node(15330)); 277 | 278 | // This should be num nodes * num replicas 279 | assert_eq!(60, hash_ring.sorted_keys.len()); 280 | assert_eq!(60, hash_ring.ring.len()); 281 | } 282 | 283 | #[test] 284 | fn test_custom_hasher() { 285 | #[derive(Default)] 286 | struct ConstantHasher; 287 | 288 | impl Hasher for ConstantHasher { 289 | fn write(&mut self, _bytes: &[u8]) { 290 | // Do nothing 291 | } 292 | 293 | fn finish(&self) -> u64 { 294 | return 1; 295 | } 296 | } 297 | 298 | type ConstantBuildHasher = BuildHasherDefault; 299 | 300 | let mut nodes: Vec = Vec::new(); 301 | nodes.push(node(15324)); 302 | nodes.push(node(15325)); 303 | nodes.push(node(15326)); 304 | nodes.push(node(15327)); 305 | nodes.push(node(15328)); 306 | nodes.push(node(15329)); 307 | 308 | let hash_ring: HashRing = 309 | HashRing::with_hasher(nodes, 10, ConstantBuildHasher::default()); 310 | 311 | assert_eq!(Some(&node(15329)), hash_ring.get_node("hello".to_string())); 312 | assert_eq!(Some(&node(15329)), hash_ring.get_node("dude".to_string())); 313 | assert_eq!(Some(&node(15329)), hash_ring.get_node("two".to_string())); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![crate_name = "hash_ring"] 2 | #![crate_type = "lib"] 3 | 4 | extern crate twox_hash; 5 | mod hash_ring; 6 | pub use hash_ring::HashRing; 7 | pub use hash_ring::NodeInfo; 8 | --------------------------------------------------------------------------------