1use std::{
13 collections::HashMap,
14 fmt,
15 hash::{BuildHasher as _, RandomState},
16 sync::{
17 Arc, Mutex, MutexGuard, PoisonError,
18 atomic::{AtomicU64, Ordering},
19 },
20 time::Duration,
21};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct PartitionerConfig {
26 pub sticky_batch_size: usize,
29 pub ignore_keys: bool,
31 pub adaptive_partitioning: bool,
33 pub availability_timeout: Duration,
36 pub rack_aware: bool,
41}
42
43impl Default for PartitionerConfig {
44 fn default() -> Self {
46 Self {
47 sticky_batch_size: crate::DEFAULT_PRODUCER_BATCH_BYTES,
48 ignore_keys: crate::DEFAULT_PRODUCER_PARTITIONER_IGNORE_KEYS,
49 adaptive_partitioning: crate::DEFAULT_PRODUCER_PARTITIONER_ADAPTIVE_PARTITIONING_ENABLE,
50 availability_timeout: crate::DEFAULT_PRODUCER_PARTITIONER_AVAILABILITY_TIMEOUT,
51 rack_aware: crate::DEFAULT_PRODUCER_PARTITIONER_RACK_AWARE,
52 }
53 }
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct PartitionInfo {
60 pub partition: i32,
62 pub leader_id: Option<i32>,
65 pub leader_rack: Option<String>,
68}
69
70pub trait Partitioner: Send + Sync {
81 fn partition(
87 &self,
88 topic: &str,
89 key: Option<&[u8]>,
90 value: Option<&[u8]>,
91 available_partitions: &[PartitionInfo],
92 all_partitions: &[PartitionInfo],
93 ) -> i32;
94}
95
96pub type LeaderRack<'a> = &'a (dyn Fn(i32) -> Option<String> + Sync);
100
101#[derive(Clone, Copy)]
104pub struct RackPreference<'a> {
105 pub leader_rack: LeaderRack<'a>,
106 pub client_rack: &'a str,
107}
108
109pub type RandomSource = Arc<dyn Fn() -> u32 + Send + Sync>;
111
112fn random_u32() -> u32 {
117 let bits = RandomState::new().hash_one(0_u8) >> 32;
118 u32::try_from(bits).unwrap_or(u32::MAX)
119}
120
121pub struct TopicPartitions<'a> {
123 pub count: i32,
125 pub has_leader: &'a (dyn Fn(i32) -> bool + Sync),
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub struct StickyPartition {
136 partition: i32,
137 pick: u64,
138}
139
140impl StickyPartition {
141 pub(crate) const fn partition(self) -> i32 {
142 self.partition
143 }
144}
145
146#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct QueueSizes {
153 pub sizes: Vec<u32>,
154 pub partition_ids: Vec<i32>,
155 pub all: usize,
157}
158
159#[derive(Clone, Debug, PartialEq, Eq)]
161struct LoadStats {
162 cumulative_frequency: Vec<u32>,
163 partition_ids: Vec<i32>,
164}
165
166impl LoadStats {
167 fn from_queue_sizes(queues: QueueSizes) -> Option<Self> {
173 let QueueSizes {
174 sizes,
175 partition_ids,
176 all,
177 } = queues;
178 if sizes.is_empty() || all < 2 || sizes.len() != partition_ids.len() {
179 return None;
180 }
181 let largest = sizes.iter().copied().max()?;
182 if sizes.iter().all(|&size| size == largest) && sizes.len() == all {
183 return None;
184 }
185 let weight_base = largest.saturating_add(1);
190 let mut running = 0_u32;
191 let cumulative_frequency = sizes
192 .iter()
193 .map(|&size| {
194 running = running.saturating_add(weight_base - size);
195 running
196 })
197 .collect();
198 Some(Self {
199 cumulative_frequency,
200 partition_ids,
201 })
202 }
203}
204
205#[derive(Debug, Default)]
207struct TopicState {
208 sticky: Option<(StickyPartition, usize)>,
210 load_stats: Option<LoadStats>,
211}
212
213pub struct BuiltInPartitioner {
215 config: PartitionerConfig,
216 random: RandomSource,
217 picks: AtomicU64,
218 topics: Mutex<HashMap<String, TopicState>>,
220}
221
222impl fmt::Debug for BuiltInPartitioner {
223 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
224 formatter
225 .debug_struct("BuiltInPartitioner")
226 .field("config", &self.config)
227 .finish_non_exhaustive()
228 }
229}
230
231impl BuiltInPartitioner {
232 pub(crate) fn new(config: PartitionerConfig) -> Self {
233 Self::with_random(config, Arc::new(random_u32))
234 }
235
236 pub(crate) fn with_random(config: PartitionerConfig, random: RandomSource) -> Self {
238 Self {
239 config,
240 random,
241 picks: AtomicU64::new(0),
242 topics: Mutex::new(HashMap::new()),
243 }
244 }
245
246 pub(crate) const fn config(&self) -> PartitionerConfig {
247 self.config
248 }
249
250 pub(crate) fn keyed_partition(&self, key: Option<&[u8]>, count: i32) -> Option<i32> {
253 key.filter(|_| !self.config.ignore_keys)
254 .map(|key| partition_for_key(key, count))
255 }
256
257 fn lock(&self) -> MutexGuard<'_, HashMap<String, TopicState>> {
258 self.topics.lock().unwrap_or_else(PoisonError::into_inner)
259 }
260
261 fn with_topic<R>(&self, topic: &str, f: impl FnOnce(&mut TopicState) -> R) -> Option<R> {
263 self.lock().get_mut(topic).map(f)
264 }
265
266 pub(crate) fn peek(&self, topic: &str, partitions: &TopicPartitions<'_>) -> StickyPartition {
269 self.peek_inner(topic, partitions, None)
270 }
271
272 pub(crate) fn peek_rack_aware(
278 &self,
279 topic: &str,
280 partitions: &TopicPartitions<'_>,
281 rack: RackPreference<'_>,
282 ) -> StickyPartition {
283 self.peek_inner(topic, partitions, Some(rack))
284 }
285
286 fn peek_inner(
287 &self,
288 topic: &str,
289 partitions: &TopicPartitions<'_>,
290 rack: Option<RackPreference<'_>>,
291 ) -> StickyPartition {
292 self.peek_locked(
293 self.lock().entry(topic.to_owned()).or_default(),
294 partitions,
295 rack,
296 )
297 }
298
299 fn peek_locked(
301 &self,
302 state: &mut TopicState,
303 partitions: &TopicPartitions<'_>,
304 rack: Option<RackPreference<'_>>,
305 ) -> StickyPartition {
306 if let Some((sticky, _)) = state.sticky {
307 return sticky;
308 }
309 let sticky = self.pick(state.load_stats.as_ref(), partitions, rack);
310 state.sticky = Some((sticky, 0));
311 sticky
312 }
313
314 pub(crate) fn is_changed(&self, topic: &str, sticky: StickyPartition) -> bool {
317 self.lock()
318 .get(topic)
319 .and_then(|state| state.sticky)
320 .is_none_or(|(current, _)| current != sticky)
321 }
322
323 pub(crate) fn update(
332 &self,
333 topic: &str,
334 sticky: StickyPartition,
335 appended: usize,
336 partitions: &TopicPartitions<'_>,
337 enable_switch: bool,
338 ) {
339 self.update_inner(topic, sticky, appended, partitions, enable_switch, None);
340 }
341
342 pub(crate) fn update_rack_aware(
345 &self,
346 topic: &str,
347 sticky: StickyPartition,
348 appended: usize,
349 partitions: &TopicPartitions<'_>,
350 enable_switch: bool,
351 rack: RackPreference<'_>,
352 ) {
353 self.update_inner(
354 topic,
355 sticky,
356 appended,
357 partitions,
358 enable_switch,
359 Some(rack),
360 );
361 }
362
363 fn update_inner(
364 &self,
365 topic: &str,
366 sticky: StickyPartition,
367 appended: usize,
368 partitions: &TopicPartitions<'_>,
369 enable_switch: bool,
370 rack: Option<RackPreference<'_>>,
371 ) {
372 let size = self.config.sticky_batch_size.max(1);
373 self.with_topic(topic, |state| {
374 let Some((current, produced)) = state.sticky.as_mut() else {
375 return;
376 };
377 if *current != sticky {
378 return;
379 }
380 *produced = produced.saturating_add(appended);
381 if (*produced >= size && enable_switch) || *produced >= size.saturating_mul(2) {
382 let next = self.pick(state.load_stats.as_ref(), partitions, rack);
383 state.sticky = Some((next, 0));
384 }
385 });
386 }
387
388 pub(crate) fn update_load_stats(&self, topic: &str, queues: Option<QueueSizes>) {
391 let load_stats = queues.and_then(LoadStats::from_queue_sizes);
392 let mut topics = self.lock();
393 match topics.get_mut(topic) {
394 Some(state) => state.load_stats = load_stats,
395 None if load_stats.is_some() => {
396 topics.insert(
397 topic.to_owned(),
398 TopicState {
399 sticky: None,
400 load_stats,
401 },
402 );
403 }
404 None => {}
405 }
406 }
407
408 fn pick(
412 &self,
413 load_stats: Option<&LoadStats>,
414 partitions: &TopicPartitions<'_>,
415 rack: Option<RackPreference<'_>>,
416 ) -> StickyPartition {
417 let random = (self.random)() & 0x7fff_ffff;
419 let rack_pick = self
420 .rack_aware_candidates(partitions, rack)
421 .and_then(|candidates| Self::pick_among(load_stats, &candidates, random));
422 let partition = match (rack_pick, load_stats) {
423 (Some(partition), _) => partition,
424 (None, Some(stats)) => {
425 let total = stats
426 .cumulative_frequency
427 .last()
428 .copied()
429 .unwrap_or(1)
430 .max(1);
431 let weighted = random % total;
432 let index = stats
435 .cumulative_frequency
436 .partition_point(|&sum| sum <= weighted);
437 stats.partition_ids.get(index).copied().unwrap_or(0)
438 }
439 (None, None) => {
440 let available: Vec<i32> = (0..partitions.count)
441 .filter(|&partition| (partitions.has_leader)(partition))
442 .collect();
443 let random = usize::try_from(random).unwrap_or(0);
444 if let Some(&partition) = random
445 .checked_rem(available.len())
446 .and_then(|index| available.get(index))
447 {
448 partition
449 } else {
450 let count = usize::try_from(partitions.count.max(1)).unwrap_or(1);
453 i32::try_from(random % count).unwrap_or(0)
454 }
455 }
456 };
457 StickyPartition {
458 partition,
459 pick: self.picks.fetch_add(1, Ordering::AcqRel),
460 }
461 }
462
463 fn rack_aware_candidates(
469 &self,
470 partitions: &TopicPartitions<'_>,
471 rack: Option<RackPreference<'_>>,
472 ) -> Option<Vec<i32>> {
473 if !self.config.rack_aware {
474 return None;
475 }
476 let RackPreference {
477 leader_rack,
478 client_rack,
479 } = rack?;
480 let in_rack: Vec<i32> = (0..partitions.count)
481 .filter(|&partition| (partitions.has_leader)(partition))
482 .filter(|&partition| leader_rack(partition).as_deref() == Some(client_rack))
483 .collect();
484 (!in_rack.is_empty()).then_some(in_rack)
485 }
486
487 fn pick_among(load_stats: Option<&LoadStats>, candidates: &[i32], random: u32) -> Option<i32> {
496 if let Some(stats) = load_stats {
497 let mut previous_cumulative = 0_u32;
498 let mut running = 0_u32;
499 let mut cumulative_frequency = Vec::new();
500 let mut partition_ids = Vec::new();
501 for (index, &partition_id) in stats.partition_ids.iter().enumerate() {
502 let cumulative = stats.cumulative_frequency.get(index).copied().unwrap_or(0);
503 let weight = cumulative.saturating_sub(previous_cumulative);
504 previous_cumulative = cumulative;
505 if candidates.contains(&partition_id) {
506 running = running.saturating_add(weight);
507 cumulative_frequency.push(running);
508 partition_ids.push(partition_id);
509 }
510 }
511 if !partition_ids.is_empty() {
512 let total = cumulative_frequency.last().copied().unwrap_or(1).max(1);
513 let weighted = random % total;
514 let index = cumulative_frequency.partition_point(|&sum| sum <= weighted);
515 return partition_ids
516 .get(index)
517 .or_else(|| candidates.first())
518 .copied();
519 }
520 }
521 let index = usize::try_from(random)
522 .unwrap_or(0)
523 .checked_rem(candidates.len())?;
524 candidates.get(index).copied()
525 }
526}
527
528#[must_use]
540pub fn partition_for_key(key: &[u8], num_partitions: i32) -> i32 {
541 assert2::assert!(num_partitions > 0);
542 (murmur2(key) & 0x7fff_ffff) % num_partitions
545}
546
547fn murmur2(data: &[u8]) -> i32 {
552 const SEED: u32 = 0x9747_b28c;
553 const M: u32 = 0x5bd1_e995;
554 const R: u32 = 24;
555
556 let length = data.len();
557 let [b0, b1, b2, b3, ..] = length.to_le_bytes();
560 let length_low = u32::from_le_bytes([b0, b1, b2, b3]);
561 let mut h: u32 = SEED ^ length_low;
562
563 let (chunks, remainder) = data.as_chunks::<4>();
564 for chunk in chunks {
565 let mut k = u32::from_le_bytes(*chunk);
566 k = k.wrapping_mul(M);
567 k ^= k >> R;
568 k = k.wrapping_mul(M);
569 h = h.wrapping_mul(M);
570 h ^= k;
571 }
572
573 match *remainder {
574 [r0, r1, r2] => {
575 h ^= u32::from(r2) << 16;
576 h ^= u32::from(r1) << 8;
577 h ^= u32::from(r0);
578 h = h.wrapping_mul(M);
579 }
580 [r0, r1] => {
581 h ^= u32::from(r1) << 8;
582 h ^= u32::from(r0);
583 h = h.wrapping_mul(M);
584 }
585 [r0] => {
586 h ^= u32::from(r0);
587 h = h.wrapping_mul(M);
588 }
589 _ => {}
590 }
591
592 h ^= h >> 13;
593 h = h.wrapping_mul(M);
594 h ^= h >> 15;
595
596 h.cast_signed()
598}
599
600#[cfg(test)]
601mod tests {
602 use std::{collections::VecDeque, iter};
603
604 use assert2::check;
605
606 use super::*;
607
608 const TOPIC: &str = "t";
609
610 fn config(sticky_batch_size: usize) -> PartitionerConfig {
611 PartitionerConfig {
612 sticky_batch_size,
613 ignore_keys: false,
614 adaptive_partitioning: true,
615 availability_timeout: Duration::ZERO,
616 rack_aware: false,
617 }
618 }
619
620 fn scripted(config: PartitionerConfig, values: &[u32]) -> BuiltInPartitioner {
622 let values = Arc::new(Mutex::new(values.iter().copied().collect::<VecDeque<_>>()));
623 BuiltInPartitioner::with_random(
624 config,
625 Arc::new(move || {
626 values
627 .lock()
628 .unwrap()
629 .pop_front()
630 .expect("the test scripts enough random values")
631 }),
632 )
633 }
634
635 fn all_leaders(_: i32) -> bool {
636 true
637 }
638
639 #[test]
640 fn a_new_sticky_partition_is_random_among_partitions_with_a_leader() {
641 let cases: [(&str, i32, &[i32], u32, i32); 7] = [
646 ("zero", 6, &[0, 1, 2, 3, 4, 5], 0, 0),
647 ("value 7 of 6", 6, &[0, 1, 2, 3, 4, 5], 7, 1),
648 ("sign bit masked", 6, &[0, 1, 2, 3, 4, 5], 0x8000_0005, 5),
649 ("p1 and p2 have no leader, value 0", 4, &[0, 3], 0, 0),
650 ("p1 and p2 have no leader, value 1", 4, &[0, 3], 1, 3),
651 ("p1 and p2 have no leader, value 5", 4, &[0, 3], 5, 3),
652 ("no partition has a leader", 4, &[], 6, 2),
653 ];
654 let mut actual = Vec::new();
655 let mut expected = Vec::new();
656 for (name, count, with_leader, random, want) in cases {
657 let partitioner = scripted(config(100), &[random]);
658 let has_leader = |partition: i32| with_leader.contains(&partition);
659 let partitions = TopicPartitions {
660 count,
661 has_leader: &has_leader,
662 };
663 let first = partitioner.peek(TOPIC, &partitions).partition();
664 let second = partitioner.peek(TOPIC, &partitions).partition();
666 actual.push((name, first, second));
667 expected.push((name, want, want));
668 }
669 assert2::assert!(actual == expected);
670 }
671
672 #[test]
673 fn the_sticky_partition_switches_after_batch_size_bytes() {
674 type SwitchRow = (&'static str, &'static [(usize, bool)], &'static [i32]);
679 let cases: [SwitchRow; 5] = [
680 (
681 "no switch before batch.size",
682 &[(30, true), (30, true), (30, true)],
683 &[1, 1, 1],
684 ),
685 (
686 "switch once batch.size bytes went to the partition",
687 &[(30, true), (30, true), (30, true), (30, true), (30, true)],
688 &[1, 1, 1, 2, 2],
689 ),
690 (
691 "a batch that is not full defers the switch",
692 &[(60, false), (60, false), (0, true)],
693 &[1, 1, 2],
694 ),
695 (
696 "twice batch.size switches with the switch off",
697 &[(60, false), (60, false), (60, false), (60, false)],
698 &[1, 1, 1, 2],
699 ),
700 (
701 "a new partition counts from zero",
702 &[(100, true), (99, true), (1, true)],
703 &[2, 2, 3],
704 ),
705 ];
706 let mut actual = Vec::new();
707 let mut expected = Vec::new();
708 for (name, appends, want) in cases {
709 let partitioner = scripted(config(100), &[1, 2, 3]);
710 let partitions = TopicPartitions {
711 count: 4,
712 has_leader: &all_leaders,
713 };
714 let mut after = Vec::new();
715 for &(bytes, enable_switch) in appends {
716 let sticky = partitioner.peek(TOPIC, &partitions);
717 partitioner.update(TOPIC, sticky, bytes, &partitions, enable_switch);
718 after.push(partitioner.peek(TOPIC, &partitions).partition());
719 }
720 actual.push((name, after));
721 expected.push((name, want.to_vec()));
722 }
723 assert2::assert!(actual == expected);
724 }
725
726 #[test]
727 fn a_stale_sticky_partition_is_a_change_and_does_not_count_bytes() {
728 let partitioner = scripted(config(100), &[2, 2]);
729 let partitions = TopicPartitions {
730 count: 4,
731 has_leader: &all_leaders,
732 };
733 let old = partitioner.peek(TOPIC, &partitions);
734 partitioner.update(TOPIC, old, 100, &partitions, true);
735 let new = partitioner.peek(TOPIC, &partitions);
736 partitioner.update(TOPIC, old, 100, &partitions, true);
739 check!(
740 (
741 old.partition(),
742 new.partition(),
743 partitioner.is_changed(TOPIC, old),
744 partitioner.is_changed(TOPIC, new),
745 partitioner.peek(TOPIC, &partitions),
746 ) == (2, 2, true, false, new)
747 );
748 }
749
750 #[test]
751 fn keys_pick_the_partition_unless_ignore_keys_is_set() {
752 let cases = [
753 ("keyed", false, Some(b"kafka".as_slice()), Some(0)),
754 ("keyless", false, None, None),
755 (
756 "keyed with ignore keys",
757 true,
758 Some(b"kafka".as_slice()),
759 None,
760 ),
761 ];
762 for (name, ignore_keys, key, want) in cases {
763 let partitioner = BuiltInPartitioner::new(PartitionerConfig {
764 ignore_keys,
765 ..config(100)
766 });
767 check!(partitioner.keyed_partition(key, 4) == want, "{name}");
768 }
769 }
770
771 #[test]
772 fn distinct_topics_have_distinct_sticky_partitions() {
773 let partitioner = scripted(config(100), &[1, 3]);
774 let partitions = TopicPartitions {
775 count: 4,
776 has_leader: &all_leaders,
777 };
778 let a = partitioner.peek("a", &partitions).partition();
779 let b = partitioner.peek("b", &partitions).partition();
780 check!((a, b) == (1, 3));
781 }
782
783 #[test]
784 fn load_stats_follow_kafka() {
785 let queues = |sizes: &[u32], partition_ids: &[i32], all: usize| QueueSizes {
786 sizes: sizes.to_vec(),
787 partition_ids: partition_ids.to_vec(),
788 all,
789 };
790 let stats = |cumulative_frequency: &[u32], partition_ids: &[i32]| {
791 Some(LoadStats {
792 cumulative_frequency: cumulative_frequency.to_vec(),
793 partition_ids: partition_ids.to_vec(),
794 })
795 };
796 let cases = [
797 (
798 "Kafka's example",
799 queues(&[0, 3, 1], &[0, 1, 2], 3),
800 stats(&[4, 5, 8], &[0, 1, 2]),
801 ),
802 ("equal queues", queues(&[2, 2, 2], &[0, 1, 2], 3), None),
803 (
804 "equal queues, one partition excluded",
805 queues(&[2, 2], &[0, 2], 3),
806 stats(&[1, 2], &[0, 2]),
807 ),
808 ("one partition", queues(&[5], &[0], 1), None),
809 (
810 "one partition left of two",
811 queues(&[5], &[1], 2),
812 stats(&[1], &[1]),
813 ),
814 ("no partition left", queues(&[], &[], 3), None),
815 ];
816 for (name, queues, want) in cases {
817 check!(LoadStats::from_queue_sizes(queues) == want, "{name}");
818 }
819 }
820
821 #[test]
822 fn adaptive_pick_weights_by_queue_size() {
823 let partitioner = scripted(config(1), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 0]);
828 let partitions = TopicPartitions {
829 count: 7,
830 has_leader: &all_leaders,
831 };
832 partitioner.update_load_stats(
833 TOPIC,
834 Some(QueueSizes {
835 sizes: vec![0, 3, 1],
836 partition_ids: vec![4, 5, 6],
837 all: 3,
838 }),
839 );
840 let picks: Vec<i32> = iter::repeat_with(|| {
841 let sticky = partitioner.peek(TOPIC, &partitions);
842 partitioner.update(TOPIC, sticky, 1, &partitions, true);
843 sticky.partition()
844 })
845 .take(9)
846 .collect();
847 check!(picks == vec![4, 4, 4, 4, 5, 6, 6, 6, 4]);
848 }
849
850 fn two_rack_leaders(partition: i32) -> Option<String> {
853 match partition {
854 0 | 1 => Some("a".to_owned()),
855 2 | 3 => Some("b".to_owned()),
856 _ => None,
857 }
858 }
859
860 fn in_rack(client_rack: &str) -> RackPreference<'_> {
863 RackPreference {
864 leader_rack: &two_rack_leaders,
865 client_rack,
866 }
867 }
868
869 #[test]
870 fn rack_aware_pick_prefers_a_partition_whose_leader_is_in_the_client_rack() {
871 let cases: [(&str, u32, i32); 4] = [
874 ("value 0 of the 2 in-rack", 0, 0),
875 ("value 1 of the 2 in-rack", 1, 1),
876 ("value 2 wraps to the first in-rack", 2, 0),
877 ("sign bit masked", 0x8000_0001, 1),
878 ];
879 let mut actual = Vec::new();
880 let mut expected = Vec::new();
881 for (name, random, want) in cases {
882 let partitioner = scripted(
883 PartitionerConfig {
884 rack_aware: true,
885 ..config(100)
886 },
887 &[random],
888 );
889 let partitions = TopicPartitions {
890 count: 4,
891 has_leader: &all_leaders,
892 };
893 let sticky = partitioner.peek_rack_aware(TOPIC, &partitions, in_rack("a"));
894 actual.push((name, sticky.partition()));
895 expected.push((name, want));
896 }
897 assert2::assert!(actual == expected);
898 }
899
900 #[test]
901 fn rack_aware_pick_falls_back_to_every_partition_with_a_leader_when_none_is_in_rack() {
902 let partitioner = scripted(
905 PartitionerConfig {
906 rack_aware: true,
907 ..config(100)
908 },
909 &[1],
910 );
911 let partitions = TopicPartitions {
912 count: 4,
913 has_leader: &all_leaders,
914 };
915 let sticky = partitioner.peek_rack_aware(TOPIC, &partitions, in_rack("z"));
916 assert2::check!(sticky.partition() == 1);
917 }
918
919 #[test]
920 fn rack_awareness_applies_only_when_the_config_and_the_client_rack_agree() {
921 let partitioner = scripted(config(100), &[3]);
924 let partitions = TopicPartitions {
925 count: 4,
926 has_leader: &all_leaders,
927 };
928 let sticky = partitioner.peek_rack_aware(TOPIC, &partitions, in_rack("a"));
929 assert2::check!(sticky.partition() == 3);
930 }
931
932 #[test]
933 fn rack_aware_adaptive_pick_weights_by_queue_size_within_the_rack() {
934 let partitioner = scripted(
939 PartitionerConfig {
940 rack_aware: true,
941 ..config(1)
942 },
943 &[0, 4],
944 );
945 let partitions = TopicPartitions {
946 count: 4,
947 has_leader: &all_leaders,
948 };
949 partitioner.update_load_stats(
950 TOPIC,
951 Some(QueueSizes {
952 sizes: vec![0, 3, 1, 2],
953 partition_ids: vec![0, 1, 2, 3],
954 all: 4,
955 }),
956 );
957 let first = partitioner.peek_rack_aware(TOPIC, &partitions, in_rack("a"));
958 partitioner.update_rack_aware(TOPIC, first, 1, &partitions, true, in_rack("a"));
959 let second = partitioner.peek_rack_aware(TOPIC, &partitions, in_rack("a"));
960 assert2::check!((first.partition(), second.partition()) == (0, 1));
961 }
962
963 #[test]
964 fn keyed_partitioning_matches_kafka_to_positive_not_absolute_value() {
965 for (key, partitions, want) in [
972 (b"".as_slice(), 10, 1),
973 (b"a".as_slice(), 10, 4),
974 (b"ab".as_slice(), 10, 4),
975 (b"abc".as_slice(), 10, 7),
976 (b"abcd".as_slice(), 10, 0),
977 (b"abcde".as_slice(), 10, 1),
978 (b"kafka".as_slice(), 10, 0),
979 (b"my-key".as_slice(), 10, 9),
980 (b"abcd".as_slice(), 16, 4),
981 (b"kafka".as_slice(), 16, 4),
982 (b"abcd".as_slice(), 3, 2),
983 (b"kafka".as_slice(), 3, 1),
984 ] {
985 let got = partition_for_key(key, partitions);
986 assert2::assert!(got == want, "key {key:?} over {partitions} partitions");
987 assert2::assert!(got >= 0 && got < partitions);
988 }
989 }
990
991 #[test]
992 fn a_negative_hash_masks_its_sign_bit_rather_than_negating() {
993 let hash = murmur2(b"kafka");
997 assert2::assert!(hash == -798_503_068);
998 assert2::assert!((hash & 0x7fff_ffff) == 1_348_980_580);
999 assert2::assert!(partition_for_key(b"kafka", 10) == 1_348_980_580 % 10);
1000 }
1001
1002 #[test]
1003 fn murmur2_matches_kafka_golden_vectors() {
1004 for (_name, input, want) in [
1005 ("empty", b"".as_slice(), 275_646_681),
1006 ("one byte", b"a".as_slice(), -1_563_381_124),
1007 ("two bytes", b"ab".as_slice(), 316_155_434),
1008 ("three bytes", b"abc".as_slice(), 479_470_107),
1009 ("four bytes", b"abcd".as_slice(), -1_323_649_548),
1010 ("five bytes", b"abcde".as_slice(), 461_995_741),
1011 ("kafka", b"kafka".as_slice(), -798_503_068),
1012 ("my key", b"my-key".as_slice(), 1_748_425_209),
1013 ] {
1014 assert2::assert!(murmur2(input) == want);
1015 }
1016 }
1017}