Skip to main content

sz_orm_sharding/
lib.rs

1//! # SZ-ORM Sharding — 分片路由
2//!
3//! 提供基于 FNV-1a + fmix64 终结化的确定性哈希与一致性哈希环分片路由,
4//! 保证跨进程/重启后同一 key 的路由结果一致,避免相似 key 聚集。
5//!
6//! ## 主要模块
7//!
8//! - [`enhanced`] — 增强分片能力(虚拟节点等)
9//! - [`routing`] — 分片键提取器(`ShardKeyExtractor` 等)
10//! - [`scatter`] — 跨分片聚合(Scatter-Gather)
11//! - [`cross_shard_tx`] — 跨分片事务协调(2PC / Best Effort)
12
13use serde::{Deserialize, Serialize};
14use std::collections::{HashMap, HashSet};
15use std::error::Error;
16use std::fmt;
17
18pub mod cross_shard_tx;
19pub mod enhanced;
20pub mod routing;
21pub mod scatter;
22
23#[cfg(feature = "shard-rebalance")]
24pub mod rebalancer;
25
26// 顶层再导出常用类型,方便用户直接 `use sz_orm_sharding::*`
27pub use cross_shard_tx::{
28    ShardParticipant, ShardTransactionCoordinator, ShardTxError, ShardTxResult,
29};
30pub use routing::{CompositeKeyExtractor, FieldExtractor, ShardKeyExtractor};
31pub use scatter::ScatterGather;
32
33/// FNV-1a 64-bit deterministic hash function (with MurmurHash3 fmix64 finalization).
34///
35/// Used for sharding routing to guarantee that the same key hashes to the same
36/// value across processes and restarts. Does not depend on any random seed,
37/// avoiding the nondeterminism of `DefaultHasher` (based on `RandomState`).
38///
39/// Note: pure FNV-1a has weak avalanche properties for short strings; keys with
40/// similar prefixes (e.g. `key_0`, `key_1`) produce highly correlated hashes,
41/// causing severely uneven distribution on the consistent-hash ring. The fmix64
42/// finalization step breaks this structural correlation so that hash values are
43/// approximately uniformly distributed in the 64-bit space.
44fn fnv1a_hash(data: &str) -> u64 {
45    const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
46    const FNV_PRIME: u64 = 0x100000001b3;
47    let mut hash = FNV_OFFSET_BASIS;
48    for &byte in data.as_bytes() {
49        hash ^= byte as u64;
50        hash = hash.wrapping_mul(FNV_PRIME);
51    }
52    // MurmurHash3 fmix64 终结化:保证良好雪崩特性,避免相似 key 聚集
53    hash ^= hash >> 33;
54    hash = hash.wrapping_mul(0xff51afd7ed558ccd);
55    hash ^= hash >> 33;
56    hash = hash.wrapping_mul(0xc4ceb9fe1a85ec53);
57    hash ^= hash >> 33;
58    hash
59}
60
61/// Sharding strategy.
62///
63/// Note: from v0.3.0 this is extended to a non-`Copy` enum (with new
64/// `Enum`/`List`/`Directory`/`Composite` variants that carry data). The routing
65/// behavior of the three original variants `Hash`/`Range`/`Date` remains
66/// backward compatible.
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68pub enum ShardingStrategy {
69    /// Hash sharding: hash the key and select a shard by modulo.
70    Hash,
71    /// Range sharding: select a shard by the byte-value range of the key.
72    Range,
73    /// Date sharding: select a shard by the date information (YYYY-MM-DD) in the key.
74    Date,
75    /// Enum sharding: explicit key → shard mapping; unmatched keys go to the default shard.
76    Enum {
77        /// Explicit mapping table.
78        mapping: HashMap<String, String>,
79        /// Default shard when no match.
80        default: Option<String>,
81    },
82    /// List sharding: route to `target` if the key is in the predefined set, otherwise default.
83    List {
84        /// Predefined key set.
85        keys: HashSet<String>,
86        /// Target shard on hit.
87        target: String,
88        /// Default shard on miss.
89        default: Option<String>,
90    },
91    /// Directory sharding: dynamically query a routing table (key → shard).
92    Directory {
93        /// Dynamic routing table.
94        table: HashMap<String, String>,
95    },
96    /// Composite sharding: first route by `primary` to obtain a group, then route
97    /// "group:key" with `secondary` for the second-level routing.
98    Composite {
99        /// Primary strategy (decides the group).
100        primary: Box<ShardingStrategy>,
101        /// Shard list used by the primary strategy (i.e. the set of group labels).
102        primary_shards: Vec<String>,
103        /// Secondary strategy (routes within the group).
104        secondary: Box<ShardingStrategy>,
105        /// Shard list used by the secondary strategy (final shard).
106        secondary_shards: Vec<String>,
107    },
108}
109
110/// Sharding routing error.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum ShardingError {
113    /// No shard configured; cannot route.
114    NoShardsConfigured,
115    /// `Enum`/`List`/`Directory` strategy did not match the key and no default shard is set.
116    NoMappingForKey(String),
117    /// Worker thread panicked (used in `ScatterGather` parallel scenarios).
118    ThreadPanic,
119}
120
121impl fmt::Display for ShardingError {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        match self {
124            ShardingError::NoShardsConfigured => {
125                write!(f, "ShardingRouter has no shards configured")
126            }
127            ShardingError::NoMappingForKey(key) => write!(f, "no mapping for key: {}", key),
128            ShardingError::ThreadPanic => write!(f, "worker thread panicked"),
129        }
130    }
131}
132
133impl Error for ShardingError {}
134
135/// Sharding router.
136///
137/// Routes a key to the corresponding shard according to `ShardingStrategy`.
138/// From v0.3.0 supports new strategies `Enum`/`List`/`Directory`/`Composite`.
139pub struct ShardingRouter {
140    strategy: ShardingStrategy,
141    /// Used only by Hash/Range/Date; other strategies carry their own data and ignore this field.
142    shards: Vec<String>,
143}
144
145impl ShardingRouter {
146    /// Create a router (legacy API: takes a strategy and a shard list).
147    pub fn new(strategy: ShardingStrategy, shards: Vec<&str>) -> Self {
148        Self {
149            strategy,
150            shards: shards.into_iter().map(|s| s.to_string()).collect(),
151        }
152    }
153
154    /// Construct an enum-sharding router.
155    pub fn new_enum(mapping: HashMap<String, String>, default: Option<String>) -> Self {
156        Self {
157            strategy: ShardingStrategy::Enum { mapping, default },
158            shards: vec![],
159        }
160    }
161
162    /// Construct a list-sharding router.
163    pub fn new_list(keys: HashSet<String>, target: String, default: Option<String>) -> Self {
164        Self {
165            strategy: ShardingStrategy::List {
166                keys,
167                target,
168                default,
169            },
170            shards: vec![],
171        }
172    }
173
174    /// Construct a directory-sharding router.
175    pub fn new_directory(table: HashMap<String, String>) -> Self {
176        Self {
177            strategy: ShardingStrategy::Directory { table },
178            shards: vec![],
179        }
180    }
181
182    /// Construct a composite-sharding router.
183    pub fn new_composite(
184        primary: ShardingStrategy,
185        primary_shards: Vec<String>,
186        secondary: ShardingStrategy,
187        secondary_shards: Vec<String>,
188    ) -> Self {
189        Self {
190            strategy: ShardingStrategy::Composite {
191                primary: Box::new(primary),
192                primary_shards,
193                secondary: Box::new(secondary),
194                secondary_shards,
195            },
196            shards: vec![],
197        }
198    }
199
200    /// Route a key to the corresponding shard.
201    ///
202    /// # Errors
203    ///
204    /// - Returns [`ShardingError::NoShardsConfigured`] when `shards` is empty
205    ///   under Hash/Range/Date strategies.
206    /// - Returns [`ShardingError::NoMappingForKey`] when Enum/List/Directory
207    ///   strategies miss and no default is set.
208    pub fn route(&self, key: &str) -> Result<&str, ShardingError> {
209        route_strategy(&self.strategy, &self.shards, key)
210    }
211
212    /// Route via a data object and an extractor: first extract a key from `data`,
213    /// then call `route(key)`.
214    ///
215    /// # Errors
216    ///
217    /// Returns the corresponding [`ShardingError`] when extraction or routing fails.
218    pub fn route_by_data(
219        &self,
220        data: &dyn std::any::Any,
221        extractor: &dyn ShardKeyExtractor,
222    ) -> Result<&str, ShardingError> {
223        let key = extractor.extract(data)?;
224        self.route(&key)
225    }
226
227    /// Return all shards (for broadcast queries; only valid for Hash/Range/Date).
228    pub fn query_all(&self) -> &[String] {
229        &self.shards
230    }
231
232    /// Return the current strategy (cloned).
233    pub fn strategy(&self) -> ShardingStrategy {
234        self.strategy.clone()
235    }
236
237    /// Return the number of shards.
238    pub fn shard_count(&self) -> usize {
239        self.shards.len()
240    }
241}
242
243/// Generic routing dispatch: select a shard according to the strategy.
244///
245/// Implemented as a free function so that `Composite` can reuse the same logic
246/// during recursive calls. The output lifetime `'a` is bound to `strategy` and
247/// `shards` (the result borrows from one of them) and is independent of the
248/// lifetime of `key`.
249fn route_strategy<'a>(
250    strategy: &'a ShardingStrategy,
251    shards: &'a [String],
252    key: &str,
253) -> Result<&'a str, ShardingError> {
254    match strategy {
255        ShardingStrategy::Hash => {
256            if shards.is_empty() {
257                return Err(ShardingError::NoShardsConfigured);
258            }
259            Ok(route_hash(shards, key))
260        }
261        ShardingStrategy::Range => {
262            if shards.is_empty() {
263                return Err(ShardingError::NoShardsConfigured);
264            }
265            Ok(route_range(shards, key))
266        }
267        ShardingStrategy::Date => {
268            if shards.is_empty() {
269                return Err(ShardingError::NoShardsConfigured);
270            }
271            Ok(route_date(shards, key))
272        }
273        ShardingStrategy::Enum { mapping, default } => {
274            if let Some(shard) = mapping.get(key) {
275                Ok(shard.as_str())
276            } else if let Some(d) = default {
277                Ok(d.as_str())
278            } else {
279                Err(ShardingError::NoMappingForKey(key.to_string()))
280            }
281        }
282        ShardingStrategy::List {
283            keys,
284            target,
285            default,
286        } => {
287            if keys.contains(key) {
288                Ok(target.as_str())
289            } else if let Some(d) = default {
290                Ok(d.as_str())
291            } else {
292                Err(ShardingError::NoMappingForKey(key.to_string()))
293            }
294        }
295        ShardingStrategy::Directory { table } => table
296            .get(key)
297            .map(|s| s.as_str())
298            .ok_or_else(|| ShardingError::NoMappingForKey(key.to_string())),
299        ShardingStrategy::Composite {
300            primary,
301            primary_shards,
302            secondary,
303            secondary_shards,
304        } => {
305            // 一级路由得到 group 标签
306            let group = route_strategy(primary, primary_shards, key)?;
307            // 用 "group:key" 作为二级 key,让二级策略在 group 命名空间内路由
308            let composite_key = format!("{}:{}", group, key);
309            route_strategy(secondary, secondary_shards, &composite_key)
310        }
311    }
312}
313
314/// Hash routing: hash the key and select a shard by modulo.
315///
316/// The return value is borrowed from `shards` (lifetime `'a`) and is
317/// independent of `key`.
318fn route_hash<'a>(shards: &'a [String], key: &str) -> &'a str {
319    let hash = fnv1a_hash(key);
320    let idx = (hash as usize) % shards.len();
321    &shards[idx]
322}
323
324/// Range routing: split the keyspace [0, 256) evenly across shards by the first
325/// byte of the key.
326///
327/// The return value is borrowed from `shards` (lifetime `'a`) and is
328/// independent of `key`.
329fn route_range<'a>(shards: &'a [String], key: &str) -> &'a str {
330    let first_byte = key.bytes().next().unwrap_or(0) as usize;
331    let idx = (first_byte * shards.len()) / 256;
332    &shards[idx.min(shards.len() - 1)]
333}
334
335/// Date routing: select a shard by taking the "day" component of the date
336/// information (YYYY-MM-DD) in the key modulo the number of shards.
337///
338/// The return value is borrowed from `shards` (lifetime `'a`) and is
339/// independent of `key`.
340fn route_date<'a>(shards: &'a [String], key: &str) -> &'a str {
341    if let Some(date) = extract_date(key) {
342        // 用日期中的"日"(day of month)取模
343        if let Some(day) = date.get(8..10).and_then(|s| s.parse::<usize>().ok()) {
344            if day >= 1 {
345                let idx = (day - 1) % shards.len();
346                return &shards[idx];
347            }
348        }
349        // 日期解析失败,回退到日期字符串的哈希
350        let hash = fnv1a_hash(&date);
351        let idx = (hash as usize) % shards.len();
352        return &shards[idx];
353    }
354    // 没有日期信息,回退到 key 整体哈希
355    let hash = fnv1a_hash(key);
356    let idx = (hash as usize) % shards.len();
357    &shards[idx]
358}
359
360/// Extract a date in YYYY-MM-DD format from a string.
361fn extract_date(key: &str) -> Option<String> {
362    let bytes = key.as_bytes();
363    if bytes.len() < 10 {
364        return None;
365    }
366    for i in 0..=bytes.len() - 10 {
367        if is_digit(bytes[i])
368            && is_digit(bytes[i + 1])
369            && is_digit(bytes[i + 2])
370            && is_digit(bytes[i + 3])
371            && bytes[i + 4] == b'-'
372            && is_digit(bytes[i + 5])
373            && is_digit(bytes[i + 6])
374            && bytes[i + 7] == b'-'
375            && is_digit(bytes[i + 8])
376            && is_digit(bytes[i + 9])
377        {
378            return String::from_utf8(bytes[i..i + 10].to_vec()).ok();
379        }
380    }
381    None
382}
383
384fn is_digit(b: u8) -> bool {
385    b.is_ascii_digit()
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use std::collections::HashSet;
392
393    // --- 基础测试 ---
394
395    #[test]
396    fn test_router_creation() {
397        let router = ShardingRouter::new(ShardingStrategy::Hash, vec!["shard0", "shard1"]);
398        assert_eq!(router.shard_count(), 2);
399        assert_eq!(router.strategy(), ShardingStrategy::Hash);
400    }
401
402    #[test]
403    fn test_query_all() {
404        let router = ShardingRouter::new(ShardingStrategy::Range, vec!["s1", "s2", "s3"]);
405        assert_eq!(router.query_all().len(), 3);
406        assert_eq!(router.query_all()[0], "s1");
407        assert_eq!(router.query_all()[2], "s3");
408    }
409
410    #[test]
411    fn test_empty_shards_returns_error() {
412        let router = ShardingRouter::new(ShardingStrategy::Hash, vec![]);
413        let result = router.route("any_key");
414        assert!(matches!(result, Err(ShardingError::NoShardsConfigured)));
415        if let Err(err) = result {
416            let msg = format!("{}", err);
417            assert!(
418                msg.contains("no shards configured"),
419                "error message should mention empty shards, got: {}",
420                msg
421            );
422        }
423    }
424
425    #[test]
426    fn test_single_shard_always_returns_it() {
427        let router = ShardingRouter::new(ShardingStrategy::Hash, vec!["only"]);
428        assert_eq!(router.route("any_key").unwrap(), "only");
429        assert_eq!(router.route("different").unwrap(), "only");
430
431        let router = ShardingRouter::new(ShardingStrategy::Range, vec!["only"]);
432        assert_eq!(router.route("any_key").unwrap(), "only");
433
434        let router = ShardingRouter::new(ShardingStrategy::Date, vec!["only"]);
435        assert_eq!(router.route("2026-07-18").unwrap(), "only");
436    }
437
438    // --- Hash 策略测试 ---
439
440    #[test]
441    fn test_hash_deterministic() {
442        let router = ShardingRouter::new(ShardingStrategy::Hash, vec!["s0", "s1", "s2"]);
443        // 同一 key 应总是路由到同一 shard
444        let first = router.route("user:123").unwrap();
445        for _ in 0..5 {
446            assert_eq!(
447                router.route("user:123").unwrap(),
448                first,
449                "Hash 路由应确定性"
450            );
451        }
452    }
453
454    #[test]
455    fn test_hash_different_keys_distribute() {
456        let router = ShardingRouter::new(ShardingStrategy::Hash, vec!["s0", "s1", "s2", "s3"]);
457        // 大量不同 key 应分布到多个 shard
458        let mut shards_hit = HashSet::new();
459        for i in 0..100 {
460            let key = format!("key_{}", i);
461            shards_hit.insert(router.route(&key).unwrap().to_string());
462        }
463        assert!(
464            shards_hit.len() >= 2,
465            "Hash 策略在 100 个不同 key 上应至少命中 2 个 shard,实际: {}",
466            shards_hit.len()
467        );
468    }
469
470    #[test]
471    fn test_hash_same_key_same_shard() {
472        let router = ShardingRouter::new(ShardingStrategy::Hash, vec!["s0", "s1", "s2"]);
473        let r1 = router.route("consistent_key").unwrap();
474        let r2 = router.route("consistent_key").unwrap();
475        let r3 = router.route("consistent_key").unwrap();
476        assert_eq!(r1, r2);
477        assert_eq!(r2, r3);
478    }
479
480    #[test]
481    fn test_hash_empty_key() {
482        let router = ShardingRouter::new(ShardingStrategy::Hash, vec!["s0", "s1"]);
483        let shard = router.route("").unwrap();
484        // 空 key 也应路由到某个有效 shard
485        assert!(shard == "s0" || shard == "s1");
486    }
487
488    // --- Range 策略测试 ---
489
490    #[test]
491    fn test_range_ascii_vs_non_ascii() {
492        // 2 个 shard:首字节 0-127 -> s0, 128+ -> s1
493        let router = ShardingRouter::new(ShardingStrategy::Range, vec!["s0", "s1"]);
494        // ASCII 字符首字节 0-127,路由到 s0
495        assert_eq!(router.route("Hello").unwrap(), "s0");
496        assert_eq!(router.route("world").unwrap(), "s0");
497        assert_eq!(router.route("A").unwrap(), "s0");
498        assert_eq!(router.route("a").unwrap(), "s0");
499        // 非 ASCII 字符首字节 >= 194,路由到 s1
500        assert_eq!(router.route("你好").unwrap(), "s1");
501        assert_eq!(router.route("é").unwrap(), "s1");
502    }
503
504    #[test]
505    fn test_range_different_keys_hit_different_shards() {
506        // 3 个 shard,构造能命中所有 shard 的 key
507        let router = ShardingRouter::new(ShardingStrategy::Range, vec!["s0", "s1", "s2"]);
508        let mut shards_hit = HashSet::new();
509        // 'A' = 65 -> (65*3)/256 = 0 -> s0
510        shards_hit.insert(router.route("A").unwrap().to_string());
511        // 'a' = 97 -> (97*3)/256 = 1 -> s1
512        shards_hit.insert(router.route("a").unwrap().to_string());
513        // 'é' 首字节 195 -> (195*3)/256 = 2 -> s2
514        shards_hit.insert(router.route("é").unwrap().to_string());
515        assert_eq!(
516            shards_hit.len(),
517            3,
518            "Range 策略应能命中所有 3 个 shard,实际: {:?}",
519            shards_hit
520        );
521    }
522
523    #[test]
524    fn test_range_deterministic() {
525        let router = ShardingRouter::new(ShardingStrategy::Range, vec!["s0", "s1", "s2"]);
526        let first = router.route("hello").unwrap();
527        assert_eq!(router.route("hello").unwrap(), first);
528        assert_eq!(router.route("hello").unwrap(), first);
529    }
530
531    #[test]
532    fn test_range_empty_key_uses_zero_byte() {
533        let router = ShardingRouter::new(ShardingStrategy::Range, vec!["s0", "s1"]);
534        // 空 key 的首字节视为 0,应该路由到 s0
535        assert_eq!(router.route("").unwrap(), "s0");
536    }
537
538    #[test]
539    fn test_range_keys_with_similar_prefixes_cluster() {
540        // 相似前缀的 key 应路由到相同 shard(Range 的核心特性)
541        let router = ShardingRouter::new(ShardingStrategy::Range, vec!["s0", "s1"]);
542        let shard1 = router.route("user:123").unwrap();
543        let shard2 = router.route("user:456").unwrap();
544        let shard3 = router.route("user:789").unwrap();
545        assert_eq!(shard1, shard2);
546        assert_eq!(shard2, shard3);
547    }
548
549    // --- Date 策略测试 ---
550
551    #[test]
552    fn test_date_day_based_routing() {
553        // 3 个 shard,day 1..31 取模 3
554        let router = ShardingRouter::new(ShardingStrategy::Date, vec!["s0", "s1", "s2"]);
555        // day=1 -> (1-1)%3 = 0 -> s0
556        assert_eq!(router.route("2026-07-01").unwrap(), "s0");
557        // day=2 -> (2-1)%3 = 1 -> s1
558        assert_eq!(router.route("2026-07-02").unwrap(), "s1");
559        // day=3 -> (3-1)%3 = 2 -> s2
560        assert_eq!(router.route("2026-07-03").unwrap(), "s2");
561        // day=4 -> (4-1)%3 = 0 -> s0
562        assert_eq!(router.route("2026-07-04").unwrap(), "s0");
563    }
564
565    #[test]
566    fn test_date_different_days_distribute() {
567        let router = ShardingRouter::new(ShardingStrategy::Date, vec!["s0", "s1", "s2", "s3"]);
568        let mut shards_hit = HashSet::new();
569        for day in 1..=28 {
570            let key = format!("2026-07-{:02}", day);
571            shards_hit.insert(router.route(&key).unwrap().to_string());
572        }
573        // 28 天应分布到所有 4 个 shard
574        assert_eq!(
575            shards_hit.len(),
576            4,
577            "Date 策略 28 天应命中所有 4 个 shard,实际: {}",
578            shards_hit.len()
579        );
580    }
581
582    #[test]
583    fn test_date_extract_from_longer_key() {
584        let router = ShardingRouter::new(ShardingStrategy::Date, vec!["s0", "s1", "s2"]);
585        // 日期嵌入在更长的 key 中
586        let shard1 = router.route("log:2026-07-15:entry1").unwrap();
587        let shard2 = router.route("2026-07-15").unwrap();
588        assert_eq!(shard1, shard2, "包含相同日期的 key 应路由到相同 shard");
589    }
590
591    #[test]
592    fn test_date_deterministic() {
593        let router = ShardingRouter::new(ShardingStrategy::Date, vec!["s0", "s1", "s2"]);
594        let first = router.route("2026-07-18").unwrap();
595        assert_eq!(router.route("2026-07-18").unwrap(), first);
596    }
597
598    #[test]
599    fn test_date_no_date_falls_back_to_hash() {
600        let router = ShardingRouter::new(ShardingStrategy::Date, vec!["s0", "s1", "s2"]);
601        // 没有日期信息的 key 应回退到哈希路由(仍返回有效 shard)
602        let shard = router.route("plain_key_without_date").unwrap();
603        assert!(shard == "s0" || shard == "s1" || shard == "s2");
604        // 且确定性
605        assert_eq!(router.route("plain_key_without_date").unwrap(), shard);
606    }
607
608    #[test]
609    fn test_date_different_months_same_day_same_shard() {
610        let router = ShardingRouter::new(ShardingStrategy::Date, vec!["s0", "s1", "s2"]);
611        // 同一天不同月应路由到相同 shard(因为只看 day)
612        let july_15 = router.route("2026-07-15").unwrap();
613        let aug_15 = router.route("2026-08-15").unwrap();
614        assert_eq!(july_15, aug_15);
615    }
616
617    #[test]
618    fn test_date_invalid_date_falls_back() {
619        let router = ShardingRouter::new(ShardingStrategy::Date, vec!["s0", "s1"]);
620        // "2026-00-00" 日为 00,无法解析为有效 day(parse::<usize> 得到 0,不满足 >= 1)
621        // 应回退到日期字符串的哈希
622        let shard = router.route("2026-00-00").unwrap();
623        assert!(shard == "s0" || shard == "s1");
624    }
625
626    // --- 跨策略测试 ---
627
628    #[test]
629    fn test_different_strategies_may_route_differently() {
630        let key = "2026-07-15";
631        let hash_router = ShardingRouter::new(ShardingStrategy::Hash, vec!["s0", "s1", "s2"]);
632        let date_router = ShardingRouter::new(ShardingStrategy::Date, vec!["s0", "s1", "s2"]);
633
634        // 不要求一定不同,但都应返回有效 shard
635        let hash_shard = hash_router.route(key).unwrap();
636        let date_shard = date_router.route(key).unwrap();
637        assert!(!hash_shard.is_empty());
638        assert!(!date_shard.is_empty());
639    }
640
641    // --- extract_date 单元测试 ---
642
643    #[test]
644    fn test_extract_date_pure_date() {
645        assert_eq!(extract_date("2026-07-18"), Some("2026-07-18".to_string()));
646        assert_eq!(extract_date("2025-01-01"), Some("2025-01-01".to_string()));
647    }
648
649    #[test]
650    fn test_extract_date_embedded() {
651        assert_eq!(
652            extract_date("log:2026-07-18:entry"),
653            Some("2026-07-18".to_string())
654        );
655    }
656
657    #[test]
658    fn test_extract_date_no_date() {
659        assert_eq!(extract_date("no date here"), None);
660        assert_eq!(extract_date("2026/07/18"), None);
661        assert_eq!(extract_date(""), None);
662        assert_eq!(extract_date("short"), None);
663    }
664
665    #[test]
666    fn test_extract_date_invalid_format() {
667        assert_eq!(extract_date("2026-7-18"), None); // 月需要 2 位
668        assert_eq!(extract_date("2026-07-8"), None); // 日需要 2 位
669        assert_eq!(extract_date("abcd-07-18"), None); // 年需为数字
670    }
671
672    // ==================== v0.3.0 新增策略测试 ====================
673
674    // --- Enum 策略测试 ---
675
676    #[test]
677    fn test_enum_route_hit() {
678        let mut mapping = HashMap::new();
679        mapping.insert("cn".to_string(), "shard_cn".to_string());
680        mapping.insert("us".to_string(), "shard_us".to_string());
681        mapping.insert("eu".to_string(), "shard_eu".to_string());
682        let router = ShardingRouter::new_enum(mapping, None);
683        assert_eq!(router.route("cn").unwrap(), "shard_cn");
684        assert_eq!(router.route("us").unwrap(), "shard_us");
685        assert_eq!(router.route("eu").unwrap(), "shard_eu");
686    }
687
688    #[test]
689    fn test_enum_route_miss_with_default() {
690        let mut mapping = HashMap::new();
691        mapping.insert("cn".to_string(), "shard_cn".to_string());
692        let router = ShardingRouter::new_enum(mapping, Some("shard_default".to_string()));
693        assert_eq!(router.route("unknown").unwrap(), "shard_default");
694        // 命中映射的仍返回映射值
695        assert_eq!(router.route("cn").unwrap(), "shard_cn");
696    }
697
698    #[test]
699    fn test_enum_route_miss_no_default_errors() {
700        let router = ShardingRouter::new_enum(HashMap::new(), None);
701        let result = router.route("unknown");
702        assert!(matches!(result, Err(ShardingError::NoMappingForKey(_))));
703        if let Err(ShardingError::NoMappingForKey(key)) = result {
704            assert_eq!(key, "unknown");
705        } else {
706            panic!("expected NoMappingForKey");
707        }
708    }
709
710    #[test]
711    fn test_enum_route_deterministic() {
712        let mut mapping = HashMap::new();
713        mapping.insert("k1".to_string(), "s_a".to_string());
714        let router = ShardingRouter::new_enum(mapping, Some("s_def".to_string()));
715        let r1 = router.route("k1").unwrap();
716        let r2 = router.route("k1").unwrap();
717        assert_eq!(r1, r2);
718        assert_eq!(router.route("k2").unwrap(), "s_def");
719    }
720
721    // --- List 策略测试 ---
722
723    #[test]
724    fn test_list_route_hit() {
725        let mut keys = HashSet::new();
726        keys.insert("vip1".to_string());
727        keys.insert("vip2".to_string());
728        keys.insert("vip3".to_string());
729        let router = ShardingRouter::new_list(keys, "vip_shard".to_string(), None);
730        assert_eq!(router.route("vip1").unwrap(), "vip_shard");
731        assert_eq!(router.route("vip2").unwrap(), "vip_shard");
732        assert_eq!(router.route("vip3").unwrap(), "vip_shard");
733    }
734
735    #[test]
736    fn test_list_route_miss_with_default() {
737        let keys = HashSet::new();
738        let router = ShardingRouter::new_list(
739            keys,
740            "vip_shard".to_string(),
741            Some("normal_shard".to_string()),
742        );
743        // 命中列表的 key 路由到 target
744        assert_eq!(router.route("any_non_listed").unwrap(), "normal_shard");
745    }
746
747    #[test]
748    fn test_list_route_miss_no_default_errors() {
749        let router = ShardingRouter::new_list(HashSet::new(), "vip_shard".to_string(), None);
750        let result = router.route("unknown");
751        assert!(matches!(result, Err(ShardingError::NoMappingForKey(_))));
752    }
753
754    #[test]
755    fn test_list_route_with_members_and_default() {
756        let mut keys = HashSet::new();
757        keys.insert("gold".to_string());
758        let router = ShardingRouter::new_list(
759            keys,
760            "premium_shard".to_string(),
761            Some("standard_shard".to_string()),
762        );
763        assert_eq!(router.route("gold").unwrap(), "premium_shard");
764        assert_eq!(router.route("silver").unwrap(), "standard_shard");
765    }
766
767    // --- Directory 策略测试 ---
768
769    #[test]
770    fn test_directory_route_hit() {
771        let mut table = HashMap::new();
772        table.insert("user:1".to_string(), "dir_shard_a".to_string());
773        table.insert("user:2".to_string(), "dir_shard_b".to_string());
774        let router = ShardingRouter::new_directory(table);
775        assert_eq!(router.route("user:1").unwrap(), "dir_shard_a");
776        assert_eq!(router.route("user:2").unwrap(), "dir_shard_b");
777    }
778
779    #[test]
780    fn test_directory_route_miss_errors() {
781        let router = ShardingRouter::new_directory(HashMap::new());
782        assert!(matches!(
783            router.route("missing"),
784            Err(ShardingError::NoMappingForKey(_))
785        ));
786    }
787
788    #[test]
789    fn test_directory_route_deterministic() {
790        let mut table = HashMap::new();
791        table.insert("k".to_string(), "v".to_string());
792        let router = ShardingRouter::new_directory(table);
793        let r1 = router.route("k").unwrap();
794        let r2 = router.route("k").unwrap();
795        assert_eq!(r1, r2);
796        assert_eq!(r1, "v");
797    }
798
799    // --- Composite 策略测试 ---
800
801    #[test]
802    fn test_composite_route_basic() {
803        // 一级:Hash 在 [g0, g1] 上路由得 group
804        // 二级:Hash 在 [s0, s1, s2] 上路由得最终 shard
805        let router = ShardingRouter::new_composite(
806            ShardingStrategy::Hash,
807            vec!["g0".to_string(), "g1".to_string()],
808            ShardingStrategy::Hash,
809            vec!["s0".to_string(), "s1".to_string(), "s2".to_string()],
810        );
811        let result = router.route("user:123").unwrap();
812        assert!(
813            result == "s0" || result == "s1" || result == "s2",
814            "composite result should be in secondary shards, got {}",
815            result
816        );
817    }
818
819    #[test]
820    fn test_composite_route_deterministic() {
821        let router = ShardingRouter::new_composite(
822            ShardingStrategy::Hash,
823            vec!["g0".to_string(), "g1".to_string()],
824            ShardingStrategy::Hash,
825            vec!["s0".to_string(), "s1".to_string()],
826        );
827        let r1 = router.route("user:123").unwrap();
828        for _ in 0..5 {
829            assert_eq!(router.route("user:123").unwrap(), r1);
830        }
831    }
832
833    #[test]
834    fn test_composite_uses_group_in_secondary_key() {
835        // 构造两个不同 group 的场景:一级用 Enum 强制分组
836        // groupA 的所有 key 经二级 Hash 在 [s0, s1] 路由
837        // groupB 的所有 key 经二级 Hash 在 [s0, s1] 路由,但二级 key 含不同 group 前缀
838        let mut mapping = HashMap::new();
839        mapping.insert("a".to_string(), "grpA".to_string());
840        mapping.insert("b".to_string(), "grpB".to_string());
841        let router = ShardingRouter::new_composite(
842            ShardingStrategy::Enum {
843                mapping,
844                default: None,
845            },
846            vec!["grpA".to_string(), "grpB".to_string()], // primary_shards(仅占位,Enum 不使用)
847            ShardingStrategy::Hash,
848            vec!["s0".to_string(), "s1".to_string()],
849        );
850        // key "a" 与 "b" 走不同 group,二级用 "grpA:a" / "grpB:b" 路由
851        let ra = router.route("a").unwrap();
852        let rb = router.route("b").unwrap();
853        // 都应路由到有效 shard
854        assert!(ra == "s0" || ra == "s1");
855        assert!(rb == "s0" || rb == "s1");
856    }
857
858    #[test]
859    fn test_composite_primary_empty_shards_errors() {
860        // primary 用 Hash 但 primary_shards 为空 → NoShardsConfigured
861        let router = ShardingRouter::new_composite(
862            ShardingStrategy::Hash,
863            vec![],
864            ShardingStrategy::Hash,
865            vec!["s0".to_string()],
866        );
867        assert!(matches!(
868            router.route("k"),
869            Err(ShardingError::NoShardsConfigured)
870        ));
871    }
872
873    #[test]
874    fn test_composite_secondary_empty_shards_errors() {
875        // secondary 用 Hash 但 secondary_shards 为空 → NoShardsConfigured
876        let router = ShardingRouter::new_composite(
877            ShardingStrategy::Hash,
878            vec!["g0".to_string()],
879            ShardingStrategy::Hash,
880            vec![],
881        );
882        assert!(matches!(
883            router.route("k"),
884            Err(ShardingError::NoShardsConfigured)
885        ));
886    }
887
888    // --- ShardingError 新增变体测试 ---
889
890    #[test]
891    fn test_sharding_error_no_mapping_display() {
892        let err = ShardingError::NoMappingForKey("k1".to_string());
893        let msg = format!("{}", err);
894        assert!(msg.contains("no mapping for key: k1"));
895    }
896
897    #[test]
898    fn test_sharding_error_thread_panic_display() {
899        let err = ShardingError::ThreadPanic;
900        let msg = format!("{}", err);
901        assert!(msg.contains("panicked"));
902    }
903}