Skip to main content

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}