rocketmq_common/common/consistenthash/consistent_hash_router.rs
1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Consistent hash router implementation
16//!
17//! To hash Node objects to a hash ring with a certain amount of virtual nodes.
18//! Method `route_node` will return a Node instance which the object key should be
19//! allocated to according to consistent hash algorithm.
20
21use std::collections::BTreeMap;
22use std::sync::Arc;
23
24use md5::Digest;
25use rocketmq_error::RocketMQError;
26use rocketmq_error::RocketMQResult;
27
28use super::hash_function::HashFunction;
29use super::node::Node;
30use super::virtual_node::VirtualNode;
31
32/// Consistent hash router that maps keys to nodes on a hash ring
33///
34/// This implementation uses virtual nodes to improve key distribution
35/// across physical nodes. Each physical node is represented by multiple
36/// virtual nodes on the ring.
37///
38/// # Type Parameters
39/// * `T` - The type of physical nodes, must implement `Node` and `Clone` traits
40///
41/// # Example
42/// ```
43/// use rocketmq_common::common::consistenthash::ConsistentHashRouter;
44/// use rocketmq_common::common::consistenthash::Node;
45///
46/// #[derive(Clone)]
47/// struct ServerNode {
48/// address: String,
49/// }
50///
51/// impl Node for ServerNode {
52/// fn get_key(&self) -> &str {
53/// &self.address
54/// }
55/// }
56///
57/// let nodes = vec![
58/// ServerNode {
59/// address: "192.168.1.1:8080".to_string(),
60/// },
61/// ServerNode {
62/// address: "192.168.1.2:8080".to_string(),
63/// },
64/// ];
65///
66/// let router = ConsistentHashRouter::new(nodes, 10);
67/// let target_node = router.route_node("my-key").unwrap();
68/// println!("Key 'my-key' routes to: {}", target_node.get_key());
69/// ```
70#[derive(Clone)]
71pub struct ConsistentHashRouter<T: Node + Clone> {
72 /// The hash ring mapping hash values to virtual nodes
73 ring: BTreeMap<i64, VirtualNode<T>>,
74 /// Hash function used to hash keys
75 hash_function: Arc<dyn HashFunction>,
76}
77
78impl<T: Node + Clone> ConsistentHashRouter<T> {
79 /// Create a new consistent hash router with the default MD5 hash function
80 ///
81 /// # Arguments
82 /// * `physical_nodes` - Collection of physical nodes to add to the ring
83 /// * `virtual_node_count` - Number of virtual nodes per physical node
84 ///
85 /// # Returns
86 /// A new ConsistentHashRouter instance
87 ///
88 /// # Example
89 /// ```
90 /// use rocketmq_common::common::consistenthash::ConsistentHashRouter;
91 /// use rocketmq_common::common::consistenthash::Node;
92 ///
93 /// #[derive(Clone)]
94 /// struct ServerNode {
95 /// name: String,
96 /// }
97 ///
98 /// impl Node for ServerNode {
99 /// fn get_key(&self) -> &str {
100 /// &self.name
101 /// }
102 /// }
103 ///
104 /// let nodes = vec![
105 /// ServerNode {
106 /// name: "node1".to_string(),
107 /// },
108 /// ServerNode {
109 /// name: "node2".to_string(),
110 /// },
111 /// ];
112 ///
113 /// let router = ConsistentHashRouter::new(nodes, 150);
114 /// ```
115 pub fn new(physical_nodes: Vec<T>, virtual_node_count: i32) -> Self {
116 Self::new_with_hash_function(physical_nodes, virtual_node_count, Arc::new(MD5Hash::new()))
117 }
118
119 /// Create a new consistent hash router and return a typed error when the configuration is
120 /// invalid.
121 pub fn try_new(physical_nodes: Vec<T>, virtual_node_count: i32) -> RocketMQResult<Self> {
122 Self::try_new_with_hash_function(physical_nodes, virtual_node_count, Arc::new(MD5Hash::new()))
123 }
124
125 /// Create a new consistent hash router with a custom hash function
126 ///
127 /// # Arguments
128 /// * `physical_nodes` - Collection of physical nodes to add to the ring
129 /// * `virtual_node_count` - Number of virtual nodes per physical node
130 /// * `hash_function` - Custom hash function to use
131 ///
132 /// # Returns
133 /// A new ConsistentHashRouter instance
134 ///
135 /// # Invalid configuration
136 /// This method returns an empty router when `virtual_node_count` is negative. Use
137 /// [`Self::try_new_with_hash_function`] when invalid configuration must be reported to the
138 /// caller.
139 pub fn new_with_hash_function(
140 physical_nodes: Vec<T>,
141 virtual_node_count: i32,
142 hash_function: Arc<dyn HashFunction>,
143 ) -> Self {
144 if virtual_node_count < 0 {
145 return Self {
146 ring: BTreeMap::new(),
147 hash_function,
148 };
149 }
150
151 let mut router = Self {
152 ring: BTreeMap::new(),
153 hash_function,
154 };
155
156 for node in physical_nodes {
157 router.add_node(node, virtual_node_count);
158 }
159
160 router
161 }
162
163 /// Create a new consistent hash router with a custom hash function and return a typed error
164 /// when `virtual_node_count` is negative.
165 pub fn try_new_with_hash_function(
166 physical_nodes: Vec<T>,
167 virtual_node_count: i32,
168 hash_function: Arc<dyn HashFunction>,
169 ) -> RocketMQResult<Self> {
170 if virtual_node_count < 0 {
171 return Err(RocketMQError::illegal_argument(format!(
172 "illegal virtual node counts: {virtual_node_count}"
173 )));
174 }
175
176 let mut router = Self {
177 ring: BTreeMap::new(),
178 hash_function,
179 };
180
181 for node in physical_nodes {
182 router.try_add_node(node, virtual_node_count)?;
183 }
184
185 Ok(router)
186 }
187
188 /// Add a physical node to the hash ring with virtual nodes
189 ///
190 /// # Arguments
191 /// * `physical_node` - Physical node to add to the hash ring
192 /// * `virtual_node_count` - The number of virtual nodes for this physical node. Value should be
193 /// greater than or equal to 0
194 ///
195 /// # Invalid configuration
196 /// This method is a no-op when `virtual_node_count` is negative. Use [`Self::try_add_node`]
197 /// when invalid configuration must be reported to the caller.
198 ///
199 /// # Example
200 /// ```
201 /// use rocketmq_common::common::consistenthash::ConsistentHashRouter;
202 /// use rocketmq_common::common::consistenthash::Node;
203 ///
204 /// #[derive(Clone)]
205 /// struct ServerNode {
206 /// name: String,
207 /// }
208 ///
209 /// impl Node for ServerNode {
210 /// fn get_key(&self) -> &str {
211 /// &self.name
212 /// }
213 /// }
214 ///
215 /// let mut router = ConsistentHashRouter::new(vec![], 0);
216 /// let node = ServerNode {
217 /// name: "node1".to_string(),
218 /// };
219 /// router.add_node(node, 100);
220 /// ```
221 pub fn add_node(&mut self, physical_node: T, virtual_node_count: i32) {
222 let _ = self.try_add_node(physical_node, virtual_node_count);
223 }
224
225 /// Add a physical node to the hash ring, returning a typed error when `virtual_node_count` is
226 /// negative.
227 pub fn try_add_node(&mut self, physical_node: T, virtual_node_count: i32) -> RocketMQResult<()> {
228 if virtual_node_count < 0 {
229 return Err(RocketMQError::illegal_argument(format!(
230 "illegal virtual node counts: {virtual_node_count}"
231 )));
232 }
233
234 let existing_replicas = self.get_existing_replicas(&physical_node);
235 for i in 0..virtual_node_count {
236 let virtual_node = VirtualNode::new(physical_node.clone(), i + existing_replicas);
237 let hash = self.hash_function.hash(virtual_node.get_key());
238 self.ring.insert(hash, virtual_node);
239 }
240
241 Ok(())
242 }
243
244 /// Remove a physical node from the hash ring
245 ///
246 /// This removes all virtual nodes associated with the given physical node.
247 ///
248 /// # Arguments
249 /// * `physical_node` - The physical node to remove
250 ///
251 /// # Example
252 /// ```
253 /// use rocketmq_common::common::consistenthash::ConsistentHashRouter;
254 /// use rocketmq_common::common::consistenthash::Node;
255 ///
256 /// #[derive(Clone)]
257 /// struct ServerNode {
258 /// name: String,
259 /// }
260 ///
261 /// impl Node for ServerNode {
262 /// fn get_key(&self) -> &str {
263 /// &self.name
264 /// }
265 /// }
266 ///
267 /// let node = ServerNode {
268 /// name: "node1".to_string(),
269 /// };
270 /// let mut router = ConsistentHashRouter::new(vec![node.clone()], 10);
271 /// router.remove_node(&node);
272 /// assert!(router.route_node("any-key").is_none());
273 /// ```
274 pub fn remove_node(&mut self, physical_node: &T) {
275 self.ring
276 .retain(|_, virtual_node| !virtual_node.is_virtual_node_of(physical_node));
277 }
278
279 /// Route a key to a node on the hash ring
280 ///
281 /// This method finds the nearest node in clockwise direction on the hash ring
282 /// for the given key using consistent hashing algorithm.
283 ///
284 /// # Arguments
285 /// * `object_key` - The object key to find a nearest node for
286 ///
287 /// # Returns
288 /// * `Some(T)` - The physical node that the key should route to
289 /// * `None` - If the ring is empty
290 ///
291 /// # Example
292 /// ```
293 /// use rocketmq_common::common::consistenthash::ConsistentHashRouter;
294 /// use rocketmq_common::common::consistenthash::Node;
295 ///
296 /// #[derive(Clone)]
297 /// struct ServerNode {
298 /// name: String,
299 /// }
300 ///
301 /// impl Node for ServerNode {
302 /// fn get_key(&self) -> &str {
303 /// &self.name
304 /// }
305 /// }
306 ///
307 /// let nodes = vec![
308 /// ServerNode {
309 /// name: "node1".to_string(),
310 /// },
311 /// ServerNode {
312 /// name: "node2".to_string(),
313 /// },
314 /// ];
315 ///
316 /// let router = ConsistentHashRouter::new(nodes, 10);
317 /// let node = router.route_node("my-key");
318 /// assert!(node.is_some());
319 /// ```
320 pub fn route_node_ref(&self, object_key: &str) -> Option<&T> {
321 if self.ring.is_empty() {
322 return None;
323 }
324
325 let hash_val = self.hash_function.hash(object_key);
326
327 // Find the first node with hash >= hash_val (tailMap in Java)
328 let virtual_node = self
329 .ring
330 .range(hash_val..)
331 .next()
332 .or_else(|| self.ring.iter().next())
333 .map(|(_, vnode)| vnode)?;
334
335 Some(virtual_node.physical_node())
336 }
337
338 pub fn route_node(&self, object_key: &str) -> Option<T> {
339 self.route_node_ref(object_key).cloned()
340 }
341
342 /// Get the number of existing virtual nodes for a physical node
343 ///
344 /// # Arguments
345 /// * `physical_node` - The physical node to count replicas for
346 ///
347 /// # Returns
348 /// The count of virtual nodes for the given physical node
349 pub fn get_existing_replicas(&self, physical_node: &T) -> i32 {
350 self.ring
351 .values()
352 .filter(|vnode| vnode.is_virtual_node_of(physical_node))
353 .count() as i32
354 }
355
356 /// Get the total number of virtual nodes in the ring
357 ///
358 /// # Returns
359 /// The total count of virtual nodes
360 pub fn size(&self) -> usize {
361 self.ring.len()
362 }
363
364 /// Check if the ring is empty
365 ///
366 /// # Returns
367 /// `true` if there are no nodes in the ring
368 pub fn is_empty(&self) -> bool {
369 self.ring.is_empty()
370 }
371}
372
373/// Default MD5 hash function implementation
374///
375/// This is the default hash function used by ConsistentHashRouter.
376/// It computes MD5 digest and uses the first 4 bytes as a long hash value.
377#[derive(Debug, Clone)]
378pub struct MD5Hash;
379
380impl MD5Hash {
381 /// Create a new MD5Hash instance
382 pub fn new() -> Self {
383 Self
384 }
385}
386
387impl Default for MD5Hash {
388 fn default() -> Self {
389 Self::new()
390 }
391}
392
393impl HashFunction for MD5Hash {
394 /// Hash a key using MD5 algorithm
395 ///
396 /// Takes the first 4 bytes of the MD5 digest and combines them
397 /// into a 32-bit signed integer value.
398 ///
399 /// # Arguments
400 /// * `key` - The key to hash
401 ///
402 /// # Returns
403 /// A 64-bit hash value derived from the MD5 digest
404 fn hash(&self, key: &str) -> i64 {
405 let mut hasher = md5::Md5::new();
406 hasher.update(key.as_bytes());
407 let result = hasher.finalize();
408
409 let mut h: i64 = 0;
410 for i in 0..4 {
411 h <<= 8;
412 h |= (result[i] as i64) & 0xFF;
413 }
414 h
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[derive(Debug, Clone, PartialEq)]
423 struct TestNode {
424 key: String,
425 }
426
427 impl TestNode {
428 fn new(key: &str) -> Self {
429 Self { key: key.to_string() }
430 }
431 }
432
433 impl Node for TestNode {
434 fn get_key(&self) -> &str {
435 &self.key
436 }
437 }
438
439 #[test]
440 fn test_new_router() {
441 let nodes = vec![TestNode::new("node1"), TestNode::new("node2")];
442 let router = ConsistentHashRouter::new(nodes, 10);
443 assert_eq!(router.size(), 20); // 2 nodes * 10 virtual nodes each
444 }
445
446 #[test]
447 fn test_add_node() {
448 let mut router = ConsistentHashRouter::new(vec![], 0);
449 assert!(router.is_empty());
450
451 router.add_node(TestNode::new("node1"), 5);
452 assert_eq!(router.size(), 5);
453
454 router.add_node(TestNode::new("node2"), 5);
455 assert_eq!(router.size(), 10);
456 }
457
458 #[test]
459 fn test_add_node_incremental() {
460 let mut router = ConsistentHashRouter::new(vec![], 0);
461 let node = TestNode::new("node1");
462
463 router.add_node(node.clone(), 5);
464 assert_eq!(router.get_existing_replicas(&node), 5);
465
466 // Adding more virtual nodes should append, not replace
467 router.add_node(node.clone(), 3);
468 assert_eq!(router.get_existing_replicas(&node), 8);
469 }
470
471 #[test]
472 fn test_add_node_negative_count_is_noop_without_panic() {
473 let mut router = ConsistentHashRouter::new(vec![], 0);
474 router.add_node(TestNode::new("node1"), -1);
475 assert!(router.is_empty());
476 }
477
478 #[test]
479 fn test_try_add_node_negative_count_returns_error() {
480 let mut router = ConsistentHashRouter::new(vec![], 0);
481 let error = router
482 .try_add_node(TestNode::new("node1"), -1)
483 .expect_err("negative virtual node count should error");
484
485 assert!(error.to_string().contains("illegal virtual node counts: -1"));
486 assert!(router.is_empty());
487 }
488
489 #[test]
490 fn test_try_new_negative_count_returns_error() {
491 let Err(error) = ConsistentHashRouter::try_new(vec![TestNode::new("node1")], -1) else {
492 panic!("negative virtual node count should error");
493 };
494
495 assert!(error.to_string().contains("illegal virtual node counts: -1"));
496 }
497
498 #[test]
499 fn test_new_negative_count_is_empty_without_panic() {
500 let router = ConsistentHashRouter::new(vec![TestNode::new("node1")], -1);
501
502 assert!(router.is_empty());
503 }
504
505 #[test]
506 fn test_remove_node() {
507 let node1 = TestNode::new("node1");
508 let node2 = TestNode::new("node2");
509 let mut router = ConsistentHashRouter::new(vec![node1.clone(), node2.clone()], 10);
510 assert_eq!(router.size(), 20);
511
512 router.remove_node(&node1);
513 assert_eq!(router.size(), 10);
514 assert_eq!(router.get_existing_replicas(&node1), 0);
515 assert_eq!(router.get_existing_replicas(&node2), 10);
516 }
517
518 #[test]
519 fn test_route_node_empty() {
520 let router: ConsistentHashRouter<TestNode> = ConsistentHashRouter::new(vec![], 0);
521 assert!(router.route_node("any-key").is_none());
522 }
523
524 #[test]
525 fn test_route_node_single() {
526 let node = TestNode::new("node1");
527 let router = ConsistentHashRouter::new(vec![node.clone()], 10);
528 let routed = router.route_node("test-key").unwrap();
529 assert_eq!(routed.get_key(), "node1");
530 }
531
532 #[test]
533 fn test_route_node_consistency() {
534 let nodes = vec![TestNode::new("node1"), TestNode::new("node2"), TestNode::new("node3")];
535 let router = ConsistentHashRouter::new(nodes, 10);
536
537 // Same key should always route to the same node
538 let key = "consistent-key";
539 let node1 = router.route_node(key).unwrap();
540 let node2 = router.route_node(key).unwrap();
541 assert_eq!(node1.get_key(), node2.get_key());
542 }
543
544 #[test]
545 fn test_route_node_ref_matches_owned_route() {
546 let nodes = vec![TestNode::new("node1"), TestNode::new("node2"), TestNode::new("node3")];
547 let router = ConsistentHashRouter::new(nodes, 10);
548
549 let key = "borrowed-route-key";
550 let routed_ref = router.route_node_ref(key).unwrap();
551 let routed_owned = router.route_node(key).unwrap();
552
553 assert_eq!(routed_ref.get_key(), routed_owned.get_key());
554 }
555
556 #[test]
557 fn test_route_node_distribution() {
558 let nodes = vec![TestNode::new("node1"), TestNode::new("node2"), TestNode::new("node3")];
559 let router = ConsistentHashRouter::new(nodes, 100);
560
561 // Test that different keys can route to different nodes
562 let mut routed_nodes = std::collections::HashSet::new();
563 for i in 0..100 {
564 let key = format!("key-{}", i);
565 if let Some(node) = router.route_node(&key) {
566 routed_nodes.insert(node.get_key().to_string());
567 }
568 }
569
570 // With 100 keys and 3 nodes with 100 virtual nodes each,
571 // we should see distribution across nodes
572 assert!(routed_nodes.len() > 1);
573 }
574
575 #[test]
576 fn test_get_existing_replicas() {
577 let node1 = TestNode::new("node1");
578 let node2 = TestNode::new("node2");
579 let router = ConsistentHashRouter::new(vec![node1.clone(), node2.clone()], 15);
580
581 assert_eq!(router.get_existing_replicas(&node1), 15);
582 assert_eq!(router.get_existing_replicas(&node2), 15);
583
584 let node3 = TestNode::new("node3");
585 assert_eq!(router.get_existing_replicas(&node3), 0);
586 }
587
588 #[test]
589 fn test_md5_hash() {
590 let hash_fn = MD5Hash::new();
591 let hash1 = hash_fn.hash("test-key");
592 let hash2 = hash_fn.hash("test-key");
593 assert_eq!(hash1, hash2); // Same key should produce same hash
594
595 let hash3 = hash_fn.hash("different-key");
596 assert_ne!(hash1, hash3); // Different keys should (likely) produce different hashes
597 }
598
599 #[test]
600 fn test_md5_hash_deterministic() {
601 let hash_fn = MD5Hash::new();
602 // Test that hash is deterministic
603 for _ in 0..10 {
604 let hash = hash_fn.hash("deterministic-test");
605 assert_eq!(hash, hash_fn.hash("deterministic-test"));
606 }
607 }
608}