1use std::collections::BTreeMap;
17use std::fmt;
18use std::num::NonZeroU16;
19use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
20use std::sync::Arc;
21
22use parking_lot::{RwLock, RwLockReadGuard};
23use serde::{Deserialize, Serialize};
24
25use crate::checkpoint::{CheckpointWatermark, CommittedSourceHandoff, SourceHandoffState};
26use crate::state::CheckpointAttempt;
27
28pub const PARTITIONING_ABI_VERSION: u16 = 1;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
41#[serde(transparent)]
42pub struct KeyGroupCount(NonZeroU16);
43
44pub const LOCAL_KEY_GROUP_COUNT: KeyGroupCount = KeyGroupCount(NonZeroU16::MIN);
46
47pub const DEFAULT_CLUSTER_KEY_GROUP_COUNT: KeyGroupCount = match NonZeroU16::new(256) {
49 Some(value) => KeyGroupCount(value),
50 None => unreachable!(),
51};
52
53pub const MAX_KEY_GROUP_COUNT: u32 = u16::MAX as u32;
55
56impl KeyGroupCount {
57 #[must_use]
59 pub const fn new(value: NonZeroU16) -> Self {
60 Self(value)
61 }
62
63 #[must_use]
65 pub const fn get(self) -> u16 {
66 self.0.get()
67 }
68
69 #[must_use]
71 pub const fn into_non_zero(self) -> NonZeroU16 {
72 self.0
73 }
74}
75
76impl fmt::Display for KeyGroupCount {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 self.get().fmt(f)
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
84#[error("key-group count {value} is outside 1..={}", u16::MAX)]
85pub struct InvalidKeyGroupCount {
86 value: u32,
87}
88
89impl InvalidKeyGroupCount {
90 #[must_use]
92 pub const fn value(self) -> u32 {
93 self.value
94 }
95}
96
97impl TryFrom<u16> for KeyGroupCount {
98 type Error = InvalidKeyGroupCount;
99
100 fn try_from(value: u16) -> Result<Self, Self::Error> {
101 NonZeroU16::new(value)
102 .map(Self)
103 .ok_or(InvalidKeyGroupCount {
104 value: u32::from(value),
105 })
106 }
107}
108
109impl TryFrom<u32> for KeyGroupCount {
110 type Error = InvalidKeyGroupCount;
111
112 fn try_from(value: u32) -> Result<Self, Self::Error> {
113 let narrowed = u16::try_from(value).map_err(|_| InvalidKeyGroupCount { value })?;
114 Self::try_from(narrowed).map_err(|_| InvalidKeyGroupCount { value })
115 }
116}
117
118impl From<NonZeroU16> for KeyGroupCount {
119 fn from(value: NonZeroU16) -> Self {
120 Self::new(value)
121 }
122}
123
124impl From<KeyGroupCount> for NonZeroU16 {
125 fn from(value: KeyGroupCount) -> Self {
126 value.into_non_zero()
127 }
128}
129
130impl From<KeyGroupCount> for u16 {
131 fn from(value: KeyGroupCount) -> Self {
132 value.get()
133 }
134}
135
136impl From<KeyGroupCount> for u32 {
137 fn from(value: KeyGroupCount) -> Self {
138 u32::from(value.get())
139 }
140}
141
142impl From<KeyGroupCount> for usize {
143 fn from(value: KeyGroupCount) -> Self {
144 usize::from(value.get())
145 }
146}
147
148#[derive(
151 Debug,
152 Clone,
153 Copy,
154 PartialEq,
155 Eq,
156 PartialOrd,
157 Ord,
158 Hash,
159 Serialize,
160 Deserialize,
161 rkyv::Archive,
162 rkyv::Serialize,
163 rkyv::Deserialize,
164)]
165pub struct NodeId(pub u64);
166
167impl NodeId {
168 pub const UNASSIGNED: Self = Self(0);
170
171 #[must_use]
173 pub const fn is_unassigned(&self) -> bool {
174 self.0 == 0
175 }
176}
177
178impl fmt::Display for NodeId {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 write!(f, "node-{}", self.0)
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum VnodeLifecycleState {
189 Active,
191 Restoring,
195}
196
197impl VnodeLifecycleState {
198 const ACTIVE: u8 = 0;
199 const RESTORING: u8 = 1;
200
201 const fn to_u8(self) -> u8 {
202 match self {
203 Self::Active => Self::ACTIVE,
204 Self::Restoring => Self::RESTORING,
205 }
206 }
207}
208
209enum SourceHandoffPublication {
210 Clear,
211 Replace(Arc<CommittedSourceHandoff>),
212 Carry,
213}
214
215#[derive(Clone)]
218pub struct VnodeAssignmentSnapshot {
219 version: u64,
220 owners: Arc<[NodeId]>,
221 owner_changed_versions: Arc<[u64]>,
222 source_handoff: Option<Arc<CommittedSourceHandoff>>,
223 source_handoff_installed_version: Option<u64>,
224}
225
226impl VnodeAssignmentSnapshot {
227 #[must_use]
229 pub const fn version(&self) -> u64 {
230 self.version
231 }
232
233 #[must_use]
235 pub fn owners(&self) -> &[NodeId] {
236 &self.owners
237 }
238
239 #[must_use]
241 pub fn owner_changed_version(&self, vnode: u32) -> Option<u64> {
242 self.owner_changed_versions.get(vnode as usize).copied()
243 }
244
245 #[must_use]
247 pub const fn has_committed_handoff(&self) -> bool {
248 self.source_handoff.is_some()
249 }
250
251 #[must_use]
253 pub fn source_handoff(&self, source: &str) -> Option<&SourceHandoffState> {
254 self.source_handoff
255 .as_deref()
256 .and_then(|handoff| handoff.source(source))
257 }
258
259 #[must_use]
261 pub fn committed_source_handoff(&self) -> Option<&CommittedSourceHandoff> {
262 self.source_handoff.as_deref()
263 }
264
265 #[must_use]
269 pub const fn source_handoff_installed_version(&self) -> Option<u64> {
270 self.source_handoff_installed_version
271 }
272
273 #[must_use]
275 pub fn source_handoff_attempt(&self) -> Option<CheckpointAttempt> {
276 self.source_handoff
277 .as_deref()
278 .map(CommittedSourceHandoff::attempt)
279 }
280
281 #[must_use]
283 pub fn source_handoff_assignment_version(&self) -> Option<u64> {
284 self.source_handoff
285 .as_deref()
286 .map(CommittedSourceHandoff::checkpoint_assignment_version)
287 }
288
289 #[must_use]
291 pub fn source_handoff_cluster_watermark(&self) -> Option<CheckpointWatermark> {
292 self.source_handoff
293 .as_deref()
294 .map(CommittedSourceHandoff::cluster_watermark)
295 }
296}
297
298pub struct VnodeAssignmentReadGuard<'a>(RwLockReadGuard<'a, VnodeAssignmentSnapshot>);
301
302impl std::ops::Deref for VnodeAssignmentReadGuard<'_> {
303 type Target = VnodeAssignmentSnapshot;
304
305 fn deref(&self) -> &Self::Target {
306 &self.0
307 }
308}
309
310fn changed_owner_versions(
311 current: &VnodeAssignmentSnapshot,
312 next_owners: &[NodeId],
313 next_version: u64,
314) -> Arc<[u64]> {
315 let skipped_generation = next_version > current.version.saturating_add(1);
316 current
317 .owners
318 .iter()
319 .zip(next_owners)
320 .enumerate()
321 .map(|(vnode, (current_owner, next_owner))| {
322 if current_owner == next_owner && !skipped_generation {
323 current.owner_changed_versions[vnode]
324 } else {
325 next_version
326 }
327 })
328 .collect::<Vec<_>>()
329 .into()
330}
331
332pub struct VnodeRegistry {
334 vnode_count: u32,
335 assignment: RwLock<VnodeAssignmentSnapshot>,
336 assignment_version: AtomicU64,
340 lifecycle: Arc<[AtomicU8]>,
346}
347
348impl std::fmt::Debug for VnodeRegistry {
349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350 f.debug_struct("VnodeRegistry")
351 .field("vnode_count", &self.vnode_count)
352 .field(
353 "assignment_version",
354 &self.assignment_version.load(Ordering::Relaxed),
355 )
356 .finish_non_exhaustive()
357 }
358}
359
360impl VnodeRegistry {
361 #[must_use]
367 pub fn new(vnode_count: u32) -> Self {
368 assert!(vnode_count > 0, "vnode_count must be > 0");
369 let assignment: Arc<[NodeId]> =
370 std::iter::repeat_n(NodeId::UNASSIGNED, vnode_count as usize)
371 .collect::<Vec<_>>()
372 .into();
373 Self {
374 vnode_count,
375 assignment: RwLock::new(VnodeAssignmentSnapshot {
376 version: 1,
377 owners: assignment,
378 owner_changed_versions: std::iter::repeat_n(1, vnode_count as usize)
379 .collect::<Vec<_>>()
380 .into(),
381 source_handoff: None,
382 source_handoff_installed_version: None,
383 }),
384 assignment_version: AtomicU64::new(1),
385 lifecycle: new_lifecycle(vnode_count),
386 }
387 }
388
389 #[must_use]
395 pub fn new_unassigned(vnode_count: u32) -> Self {
396 let registry = Self::new(vnode_count);
397 {
398 let mut initial = registry.assignment.write();
399 initial.version = 0;
400 initial.owner_changed_versions = std::iter::repeat_n(0, vnode_count as usize)
401 .collect::<Vec<_>>()
402 .into();
403 }
404 registry.assignment_version.store(0, Ordering::Release);
405 registry
406 }
407
408 #[must_use]
415 pub fn single_owner(vnode_count: u32, owner: NodeId) -> Self {
416 assert!(vnode_count > 0, "vnode_count must be > 0");
417 let assignment: Arc<[NodeId]> = std::iter::repeat_n(owner, vnode_count as usize)
418 .collect::<Vec<_>>()
419 .into();
420 Self {
421 vnode_count,
422 assignment: RwLock::new(VnodeAssignmentSnapshot {
423 version: 1,
424 owners: assignment,
425 owner_changed_versions: std::iter::repeat_n(1, vnode_count as usize)
426 .collect::<Vec<_>>()
427 .into(),
428 source_handoff: None,
429 source_handoff_installed_version: None,
430 }),
431 assignment_version: AtomicU64::new(1),
432 lifecycle: new_lifecycle(vnode_count),
433 }
434 }
435
436 #[must_use]
438 pub fn vnode_count(&self) -> u32 {
439 self.vnode_count
440 }
441
442 #[must_use]
444 pub fn assignment_version(&self) -> u64 {
445 self.assignment_version.load(Ordering::Acquire)
446 }
447
448 #[must_use]
451 pub fn owner(&self, vnode: u32) -> NodeId {
452 if vnode >= self.vnode_count {
453 return NodeId::UNASSIGNED;
454 }
455 self.assignment.read().owners[vnode as usize]
456 }
457
458 #[must_use]
461 pub fn snapshot(&self) -> Arc<[NodeId]> {
462 Arc::clone(&self.assignment.read().owners)
463 }
464
465 #[must_use]
467 pub fn versioned_snapshot(&self) -> VnodeAssignmentSnapshot {
468 self.assignment.read().clone()
469 }
470
471 #[must_use]
473 pub fn read_assignment(&self) -> VnodeAssignmentReadGuard<'_> {
474 VnodeAssignmentReadGuard(self.assignment.read())
475 }
476
477 pub fn set_assignment(&self, new_assignment: Arc<[NodeId]>) {
482 assert_eq!(
483 new_assignment.len(),
484 self.vnode_count as usize,
485 "assignment length mismatch: got {}, expected {}",
486 new_assignment.len(),
487 self.vnode_count,
488 );
489 let mut current = self.assignment.write();
490 let version = current
491 .version
492 .checked_add(1)
493 .expect("assignment version overflow");
494 let owner_changed_versions = changed_owner_versions(¤t, &new_assignment, version);
495 *current = VnodeAssignmentSnapshot {
496 version,
497 owners: new_assignment,
498 owner_changed_versions,
499 source_handoff: None,
500 source_handoff_installed_version: None,
501 };
502 self.assignment_version
503 .store(current.version, Ordering::Release);
504 }
505
506 pub fn set_assignment_and_version(&self, new_assignment: Arc<[NodeId]>, version: u64) {
514 self.publish_assignment(new_assignment, version, SourceHandoffPublication::Clear);
515 }
516
517 pub fn set_assignment_and_version_with_source_handoff(
523 &self,
524 new_assignment: Arc<[NodeId]>,
525 version: u64,
526 source_handoff: Arc<CommittedSourceHandoff>,
527 ) {
528 self.publish_assignment(
529 new_assignment,
530 version,
531 SourceHandoffPublication::Replace(source_handoff),
532 );
533 }
534
535 pub fn set_assignment_and_version_carrying_source_handoff(
538 &self,
539 new_assignment: Arc<[NodeId]>,
540 version: u64,
541 ) {
542 self.publish_assignment(new_assignment, version, SourceHandoffPublication::Carry);
543 }
544
545 fn publish_assignment(
546 &self,
547 new_assignment: Arc<[NodeId]>,
548 version: u64,
549 source_handoff: SourceHandoffPublication,
550 ) {
551 assert_eq!(
552 new_assignment.len(),
553 self.vnode_count as usize,
554 "assignment length mismatch: got {}, expected {}",
555 new_assignment.len(),
556 self.vnode_count,
557 );
558 let mut guard = self.assignment.write();
559 let current = guard.version;
560 assert!(
561 version > current,
562 "assignment version must advance: got {version}, current {current}",
563 );
564 let owner_changed_versions = changed_owner_versions(&guard, &new_assignment, version);
565 let (source_handoff, source_handoff_installed_version) = match source_handoff {
566 SourceHandoffPublication::Clear => (None, None),
567 SourceHandoffPublication::Replace(source_handoff) => {
568 (Some(source_handoff), Some(version))
569 }
570 SourceHandoffPublication::Carry => (
571 guard.source_handoff.clone(),
572 guard.source_handoff_installed_version,
573 ),
574 };
575 *guard = VnodeAssignmentSnapshot {
576 version,
577 owners: new_assignment,
578 owner_changed_versions,
579 source_handoff,
580 source_handoff_installed_version,
581 };
582 self.assignment_version.store(version, Ordering::Release);
583 }
584
585 #[must_use]
587 pub fn vnode_for_key(&self, key: &[u8]) -> u32 {
588 #[allow(clippy::cast_possible_truncation)]
589 let h = (key_hash(key) % u64::from(self.vnode_count)) as u32;
590 h
591 }
592
593 pub fn mark_restoring(&self, vnodes: &[u32]) {
599 self.set_lifecycle(vnodes, VnodeLifecycleState::Restoring);
600 }
601
602 pub fn mark_active(&self, vnodes: &[u32]) {
608 self.set_lifecycle(vnodes, VnodeLifecycleState::Active);
609 }
610
611 fn set_lifecycle(&self, vnodes: &[u32], state: VnodeLifecycleState) {
612 let byte = state.to_u8();
613 for &v in vnodes {
614 if let Some(slot) = self.lifecycle.get(v as usize) {
615 slot.store(byte, Ordering::Release);
616 }
617 }
618 }
619
620 #[must_use]
623 pub fn is_restoring(&self, vnode: u32) -> bool {
624 self.lifecycle
625 .get(vnode as usize)
626 .is_some_and(|s| s.load(Ordering::Acquire) == VnodeLifecycleState::RESTORING)
627 }
628
629 #[must_use]
632 pub fn any_restoring(&self) -> bool {
633 self.lifecycle
634 .iter()
635 .any(|s| s.load(Ordering::Acquire) == VnodeLifecycleState::RESTORING)
636 }
637
638 #[must_use]
640 #[allow(clippy::cast_possible_truncation)] pub fn restoring_vnodes(&self) -> Vec<u32> {
642 self.lifecycle
643 .iter()
644 .enumerate()
645 .filter_map(|(i, s)| {
646 (s.load(Ordering::Acquire) == VnodeLifecycleState::RESTORING).then_some(i as u32)
647 })
648 .collect()
649 }
650}
651
652fn new_lifecycle(vnode_count: u32) -> Arc<[AtomicU8]> {
654 std::iter::repeat_with(|| AtomicU8::new(VnodeLifecycleState::ACTIVE))
655 .take(vnode_count as usize)
656 .collect::<Vec<_>>()
657 .into()
658}
659
660#[must_use]
666pub fn key_hash(key: &[u8]) -> u64 {
667 xxhash_rust::xxh3::xxh3_64(key)
668}
669
670#[must_use]
678pub fn rendezvous_assignment(vnode_count: u32, peers: &[NodeId]) -> Arc<[NodeId]> {
679 assert!(
680 !peers.is_empty(),
681 "rendezvous_assignment needs at least one peer"
682 );
683 let mut sorted_peers = peers.to_vec();
684 sorted_peers.sort_by_key(|n| n.0);
685
686 let mut assignment = Vec::with_capacity(vnode_count as usize);
687 for v in 0..vnode_count {
688 let mut max_weight = 0;
689 let mut selected_node = sorted_peers[0];
690
691 for &node in &sorted_peers {
692 let mut buf = [0u8; 16];
694 buf[0..8].copy_from_slice(&u64::from(v).to_le_bytes());
695 buf[8..16].copy_from_slice(&node.0.to_le_bytes());
696 let weight = xxhash_rust::xxh3::xxh3_64(&buf);
697
698 if weight > max_weight || (weight == max_weight && node.0 > selected_node.0) {
700 max_weight = weight;
701 selected_node = node;
702 }
703 }
704 assignment.push(selected_node);
705 }
706 assignment.into()
707}
708
709#[derive(Debug, Clone, Default, PartialEq, Eq)]
713pub struct Locality {
714 tiers: Vec<String>,
715}
716
717impl Locality {
718 #[must_use]
720 pub fn new(tiers: Vec<String>) -> Self {
721 Self { tiers }
722 }
723
724 #[must_use]
727 pub fn parse(s: &str) -> Self {
728 let tiers = s
729 .split(';')
730 .map(str::trim)
731 .filter(|seg| !seg.is_empty())
732 .map(|seg| {
733 seg.split_once('=')
734 .map_or(seg, |(_, v)| v.trim())
735 .to_string()
736 })
737 .collect();
738 Self { tiers }
739 }
740
741 #[must_use]
744 pub fn domain_at(&self, tier: usize) -> String {
745 if self.tiers.is_empty() {
746 return String::new();
747 }
748 let end = tier.min(self.tiers.len() - 1);
749 self.tiers[..=end].join(";")
750 }
751}
752
753fn resolve_domains(nodes: &[(NodeId, Locality)], isolation_tier: usize) -> Vec<(NodeId, String)> {
755 nodes
756 .iter()
757 .map(|(id, loc)| (*id, loc.domain_at(isolation_tier)))
758 .collect()
759}
760
761#[must_use]
765pub fn owners_per_domain(
766 owners: &[NodeId],
767 nodes: &[(NodeId, Locality)],
768 isolation_tier: usize,
769) -> BTreeMap<String, u32> {
770 let dom: BTreeMap<NodeId, String> =
771 resolve_domains(nodes, isolation_tier).into_iter().collect();
772 let mut counts = BTreeMap::new();
773 for &o in owners {
774 *counts
775 .entry(dom.get(&o).cloned().unwrap_or_default())
776 .or_default() += 1;
777 }
778 counts
779}
780
781#[must_use]
787pub fn owned_vnodes(registry: &VnodeRegistry, owner: NodeId) -> Vec<u32> {
788 let assignment = registry.snapshot();
789 assignment
790 .iter()
791 .enumerate()
792 .filter(|(_, assigned)| **assigned == owner)
793 .filter_map(|(vnode, _)| u32::try_from(vnode).ok())
794 .collect()
795}
796
797#[must_use]
800pub fn peer_owners(registry: &VnodeRegistry, self_id: NodeId) -> Vec<NodeId> {
801 let assignment = registry.snapshot();
802 let mut peers: Vec<NodeId> = assignment
803 .iter()
804 .copied()
805 .filter(|o| !o.is_unassigned() && *o != self_id)
806 .collect();
807 peers.sort_unstable();
808 peers.dedup();
809 peers
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815 use crate::checkpoint::{
816 CheckpointAssignmentFence, CheckpointParticipant, ClusterRecoveryCapsule,
817 ConnectorCheckpoint, ParticipantRecoveryRef, PipelineIdentity,
818 CLUSTER_RECOVERY_CAPSULE_VERSION, PIPELINE_IDENTITY_VERSION,
819 };
820
821 fn digest(byte: u8) -> String {
822 format!("{byte:02x}").repeat(32)
823 }
824
825 #[test]
826 fn partitioning_key_group_count_rejects_every_out_of_range_value() {
827 assert_eq!(KeyGroupCount::try_from(0_u16).unwrap_err().value(), 0);
828 assert_eq!(KeyGroupCount::try_from(0_u32).unwrap_err().value(), 0);
829 assert_eq!(
830 KeyGroupCount::try_from(u32::from(u16::MAX) + 1)
831 .unwrap_err()
832 .value(),
833 u32::from(u16::MAX) + 1
834 );
835
836 let one = KeyGroupCount::try_from(1_u16).unwrap();
837 let max = KeyGroupCount::try_from(u32::from(u16::MAX)).unwrap();
838 assert_eq!(u16::from(one), 1);
839 assert_eq!(u32::from(max), u32::from(u16::MAX));
840 assert_eq!(usize::from(max), usize::from(u16::MAX));
841 assert_eq!(NonZeroU16::from(one), NonZeroU16::MIN);
842 }
843
844 #[test]
845 fn partitioning_abi_v1_raw_key_hash_golden_vectors() {
846 assert_eq!(PARTITIONING_ABI_VERSION, 1);
847 let actual = [
848 key_hash(b""),
849 key_hash(b"a"),
850 key_hash(b"laminardb"),
851 key_hash(&[0, 1, 0xff]),
852 key_hash("key-☃".as_bytes()),
853 ];
854 assert_eq!(
855 actual,
856 [
857 3_244_421_341_483_603_138,
858 16_629_034_431_890_738_719,
859 16_801_042_214_008_847_674,
860 10_014_172_824_849_140_082,
861 17_604_077_472_932_801_374,
862 ]
863 );
864 }
865
866 #[test]
867 fn rendezvous_placement_policy_golden_vector() {
868 let actual = rendezvous_assignment(12, &[NodeId(7), NodeId(3), NodeId(5)]);
871 assert_eq!(
872 actual.as_ref(),
873 &[
874 NodeId(5),
875 NodeId(7),
876 NodeId(3),
877 NodeId(5),
878 NodeId(5),
879 NodeId(7),
880 NodeId(5),
881 NodeId(5),
882 NodeId(5),
883 NodeId(5),
884 NodeId(5),
885 NodeId(5),
886 ]
887 );
888 }
889
890 fn committed_handoff(
891 source: Option<(&str, ConnectorCheckpoint, Option<i64>)>,
892 cluster_watermark: CheckpointWatermark,
893 ) -> Arc<CommittedSourceHandoff> {
894 let mut source_offsets = BTreeMap::new();
895 let mut source_metadata = BTreeMap::new();
896 let mut source_assignment_versions = BTreeMap::new();
897 let mut source_watermarks = BTreeMap::new();
898 if let Some((source, checkpoint, watermark)) = source {
899 let ConnectorCheckpoint {
900 offsets,
901 metadata,
902 source_assignment_version,
903 } = checkpoint;
904 source_offsets.insert(source.to_string(), offsets.into_iter().collect());
905 source_metadata.insert(source.to_string(), metadata.into_iter().collect());
906 if let Some(assignment_version) = source_assignment_version {
907 source_assignment_versions.insert(source.to_string(), assignment_version);
908 }
909 if let Some(watermark) = watermark {
910 source_watermarks.insert(source.to_string(), watermark);
911 }
912 }
913
914 let participant = CheckpointParticipant {
915 node_id: 7,
916 boot_incarnation: uuid::Uuid::from_u128(77),
917 };
918 let capsule = ClusterRecoveryCapsule {
919 version: CLUSTER_RECOVERY_CAPSULE_VERSION,
920 attempt: CheckpointAttempt::canonical(9),
921 deployment_id: uuid::Uuid::from_u128(99).to_string(),
922 pipeline_identity: PipelineIdentity {
923 canonical_version: PIPELINE_IDENTITY_VERSION,
924 sha256: digest(1),
925 },
926 assignment_fence: CheckpointAssignmentFence::from_owner_map(
927 17,
928 &[7],
929 vec![participant],
930 )
931 .unwrap(),
932 seal_inventory_sha256: digest(2),
933 participants: vec![ParticipantRecoveryRef {
934 participant_id: 7,
935 readiness_sha256: digest(3),
936 manifest_sha256: digest(4),
937 portable_state_sha256: digest(5),
938 }],
939 source_offsets,
940 source_metadata,
941 source_assignment_versions,
942 source_watermarks,
943 cluster_watermark,
944 recovery_watermark_frontier: cluster_watermark.active_value(),
945 portable_state_sha256: digest(5),
946 };
947 Arc::new(CommittedSourceHandoff::try_from(&capsule).unwrap())
948 }
949
950 #[test]
951 fn new_registry_is_unassigned() {
952 let r = VnodeRegistry::new(8);
953 assert_eq!(r.vnode_count(), 8);
954 for v in 0..8 {
955 assert!(r.owner(v).is_unassigned());
956 }
957 }
958
959 #[test]
960 fn single_owner_populates_all_slots() {
961 let r = VnodeRegistry::single_owner(4, NodeId(42));
962 for v in 0..4 {
963 assert_eq!(r.owner(v), NodeId(42));
964 }
965 }
966
967 #[test]
968 fn set_assignment_bumps_version() {
969 let r = VnodeRegistry::new(4);
970 let v0 = r.assignment_version();
971 let new_assign: Arc<[NodeId]> = vec![NodeId(1), NodeId(2), NodeId(1), NodeId(2)].into();
972 r.set_assignment(new_assign);
973 assert!(r.assignment_version() > v0);
974 assert_eq!(r.owner(0), NodeId(1));
975 assert_eq!(r.owner(1), NodeId(2));
976 }
977
978 #[test]
979 fn vnode_for_key_in_range() {
980 let r = VnodeRegistry::new(16);
981 for i in 0..100 {
982 let v = r.vnode_for_key(format!("k-{i}").as_bytes());
983 assert!(v < 16);
984 }
985 }
986
987 #[test]
988 #[should_panic(expected = "assignment length mismatch")]
989 fn set_assignment_rejects_wrong_length() {
990 let r = VnodeRegistry::new(4);
991 let bad: Arc<[NodeId]> = vec![NodeId(1)].into();
992 r.set_assignment(bad);
993 }
994
995 #[test]
996 fn owner_out_of_range_returns_unassigned() {
997 let r = VnodeRegistry::single_owner(4, NodeId(1));
998 assert!(r.owner(10).is_unassigned());
999 }
1000
1001 #[test]
1002 fn committed_empty_source_is_distinct_from_a_missing_source() {
1003 let r = VnodeRegistry::new_unassigned(4);
1004 let owners = r.snapshot();
1005 let orders = ConnectorCheckpoint {
1006 offsets: [("orders:0".into(), "41".into())].into_iter().collect(),
1007 metadata: [("topic".into(), "orders".into())].into_iter().collect(),
1008 source_assignment_version: std::num::NonZeroU64::new(17),
1009 };
1010 let version_one_handoff = committed_handoff(
1011 Some(("orders", orders, Some(700))),
1012 CheckpointWatermark::Uninitialized,
1013 );
1014 r.set_assignment_and_version_with_source_handoff(
1015 Arc::clone(&owners),
1016 1,
1017 version_one_handoff,
1018 );
1019 let version_one = r.versioned_snapshot();
1020 assert_eq!(version_one.version(), 1);
1021 assert!(version_one.has_committed_handoff());
1022 assert_eq!(
1023 version_one
1024 .source_handoff("orders")
1025 .unwrap()
1026 .checkpoint()
1027 .offsets
1028 .get("orders:0")
1029 .map(String::as_str),
1030 Some("41")
1031 );
1032 assert_eq!(
1033 version_one
1034 .source_handoff("orders")
1035 .unwrap()
1036 .checkpoint()
1037 .metadata
1038 .get("topic")
1039 .map(String::as_str),
1040 Some("orders")
1041 );
1042 assert_eq!(
1043 version_one
1044 .source_handoff("orders")
1045 .unwrap()
1046 .checkpoint()
1047 .source_assignment_version,
1048 std::num::NonZeroU64::new(17)
1049 );
1050 assert_eq!(
1051 version_one.source_handoff("orders").unwrap().watermark(),
1052 Some(700)
1053 );
1054 assert_eq!(
1055 version_one.source_handoff_cluster_watermark(),
1056 Some(CheckpointWatermark::Uninitialized)
1057 );
1058 assert_eq!(
1059 version_one.source_handoff_attempt(),
1060 Some(CheckpointAttempt::canonical(9))
1061 );
1062 assert_eq!(version_one.source_handoff_assignment_version(), Some(17));
1063 assert_eq!(version_one.source_handoff_installed_version(), Some(1));
1064
1065 let committed_empty_source = committed_handoff(
1066 Some(("orders", ConnectorCheckpoint::new(), None)),
1067 CheckpointWatermark::Idle,
1068 );
1069 r.set_assignment_and_version_with_source_handoff(owners, 2, committed_empty_source);
1070 let published = r.versioned_snapshot();
1071 assert_eq!(published.version(), 2);
1072 assert_eq!(published.source_handoff_installed_version(), Some(2));
1073 assert!(published.has_committed_handoff());
1074 let orders = published.source_handoff("orders").unwrap();
1075 assert!(orders.checkpoint().offsets.is_empty());
1076 assert!(orders.checkpoint().metadata.is_empty());
1077 assert_eq!(orders.watermark(), None);
1078 assert!(published.source_handoff("missing").is_none());
1079 assert_eq!(
1080 published.source_handoff_cluster_watermark(),
1081 Some(CheckpointWatermark::Idle)
1082 );
1083 assert_eq!(
1084 version_one
1085 .source_handoff("orders")
1086 .unwrap()
1087 .checkpoint()
1088 .offsets
1089 .get("orders:0")
1090 .map(String::as_str),
1091 Some("41"),
1092 "older immutable publications keep their version-bound handoff"
1093 );
1094 }
1095
1096 #[test]
1097 fn owner_generations_and_handoff_survive_skipped_publications() {
1098 let r = VnodeRegistry::new_unassigned(1);
1099 let self_id = NodeId(7);
1100 let other = NodeId(8);
1101 let handoff = committed_handoff(
1102 Some((
1103 "events",
1104 ConnectorCheckpoint::with_offsets(
1105 [("events:0".to_string(), "41".to_string())]
1106 .into_iter()
1107 .collect(),
1108 ),
1109 None,
1110 )),
1111 CheckpointWatermark::Idle,
1112 );
1113 r.set_assignment_and_version_with_source_handoff(
1114 vec![self_id].into(),
1115 1,
1116 Arc::clone(&handoff),
1117 );
1118 r.set_assignment_and_version_carrying_source_handoff(vec![other].into(), 2);
1119 r.set_assignment_and_version_carrying_source_handoff(vec![self_id].into(), 3);
1120
1121 let published = r.versioned_snapshot();
1122 assert_eq!(published.owner_changed_version(0), Some(3));
1123 assert!(published.has_committed_handoff());
1124 assert_eq!(published.source_handoff_installed_version(), Some(1));
1125 assert!(std::ptr::eq(
1126 published.committed_source_handoff().unwrap(),
1127 handoff.as_ref()
1128 ));
1129 assert_eq!(
1130 published
1131 .source_handoff("events")
1132 .unwrap()
1133 .checkpoint()
1134 .offsets
1135 .get("events:0")
1136 .map(String::as_str),
1137 Some("41")
1138 );
1139 }
1140
1141 #[test]
1142 fn skipped_assignment_generation_forces_owner_reconciliation() {
1143 let r = VnodeRegistry::new_unassigned(1);
1144 let self_id = NodeId(7);
1145 r.set_assignment_and_version(vec![self_id].into(), 1);
1146 assert_eq!(r.versioned_snapshot().owner_changed_version(0), Some(1));
1147
1148 r.set_assignment_and_version(vec![self_id].into(), 3);
1149 assert_eq!(
1150 r.versioned_snapshot().owner_changed_version(0),
1151 Some(3),
1152 "a missed intermediate generation may have transferred ownership"
1153 );
1154 }
1155
1156 #[test]
1157 #[should_panic(expected = "assignment version must advance")]
1158 fn assignment_publication_rejects_equal_version_mutation() {
1159 let r = VnodeRegistry::new(1);
1160 r.set_assignment_and_version(vec![NodeId(9)].into(), 1);
1161 }
1162
1163 #[test]
1164 fn vnode_for_key_is_deterministic() {
1165 let r = VnodeRegistry::new(16);
1166 assert_eq!(r.vnode_for_key(b"key-x"), r.vnode_for_key(b"key-x"));
1167 }
1168
1169 #[test]
1170 fn owned_vnodes_filters_by_owner() {
1171 let r = VnodeRegistry::new(4);
1172 r.set_assignment(vec![NodeId(1), NodeId(2), NodeId(1), NodeId(2)].into());
1173 assert_eq!(owned_vnodes(&r, NodeId(1)), vec![0, 2]);
1174 assert_eq!(owned_vnodes(&r, NodeId(2)), vec![1, 3]);
1175 assert!(owned_vnodes(&r, NodeId(99)).is_empty());
1176 }
1177
1178 #[test]
1179 fn owned_vnodes_single_owner_returns_all() {
1180 let r = VnodeRegistry::single_owner(8, NodeId(42));
1181 assert_eq!(owned_vnodes(&r, NodeId(42)), (0..8).collect::<Vec<_>>());
1182 }
1183
1184 #[test]
1185 fn rendezvous_is_deterministic() {
1186 let peers = vec![NodeId(7), NodeId(3), NodeId(5)];
1187 let assignment = rendezvous_assignment(8, &peers);
1188 let reversed = vec![NodeId(3), NodeId(5), NodeId(7)];
1190 assert_eq!(rendezvous_assignment(8, &reversed), assignment);
1191 }
1192
1193 #[test]
1194 fn rendezvous_single_peer_owns_everything() {
1195 let assignment = rendezvous_assignment(4, &[NodeId(99)]);
1196 assert!(assignment.iter().all(|&n| n == NodeId(99)));
1197 }
1198
1199 #[test]
1200 #[should_panic(expected = "needs at least one peer")]
1201 fn rendezvous_rejects_empty_peer_list() {
1202 let _ = rendezvous_assignment(4, &[]);
1203 }
1204
1205 #[test]
1206 fn rendezvous_minimizes_state_movement() {
1207 let peers3 = vec![NodeId(1), NodeId(2), NodeId(3)];
1208 let peers4 = vec![NodeId(1), NodeId(2), NodeId(3), NodeId(4)];
1209
1210 let a3 = rendezvous_assignment(256, &peers3);
1211 let a4 = rendezvous_assignment(256, &peers4);
1212
1213 let mut moved = 0;
1214 let mut moved_between_existing = 0;
1215
1216 for v in 0..256usize {
1217 let o3 = a3[v];
1218 let o4 = a4[v];
1219 if o3 != o4 {
1220 moved += 1;
1221 if o4 != NodeId(4) {
1222 moved_between_existing += 1;
1223 }
1224 }
1225 }
1226
1227 assert_eq!(
1228 moved_between_existing, 0,
1229 "No vnode should move between existing peers on a node join"
1230 );
1231 assert!(
1232 moved > 40 && moved < 90,
1233 "Expected roughly 25% of vnodes to move to the new peer, got {moved}"
1234 );
1235
1236 for v in 0..256usize {
1237 if a3[v] != a4[v] {
1238 assert_eq!(a4[v], NodeId(4));
1239 }
1240 }
1241 }
1242
1243 #[test]
1244 fn vnodes_start_active() {
1245 let r = VnodeRegistry::new(4);
1246 assert!(!r.any_restoring());
1247 for v in 0..4 {
1248 assert!(!r.is_restoring(v));
1249 }
1250 assert!(r.restoring_vnodes().is_empty());
1251 }
1252
1253 #[test]
1254 fn mark_restoring_and_active_round_trip() {
1255 let r = VnodeRegistry::new(4);
1256 r.mark_restoring(&[1, 3]);
1257 assert!(r.any_restoring());
1258 assert!(r.is_restoring(1));
1259 assert!(r.is_restoring(3));
1260 assert!(!r.is_restoring(0));
1261 assert_eq!(r.restoring_vnodes(), vec![1, 3]);
1262
1263 r.mark_active(&[1]);
1264 assert!(!r.is_restoring(1));
1265 assert_eq!(r.restoring_vnodes(), vec![3]);
1266
1267 r.mark_active(&[3]);
1268 assert!(!r.any_restoring());
1269 }
1270
1271 #[test]
1272 fn lifecycle_ignores_out_of_range() {
1273 let r = VnodeRegistry::new(2);
1274 r.mark_restoring(&[5, 99]); assert!(!r.is_restoring(5));
1276 assert!(!r.any_restoring());
1277 }
1278
1279 #[test]
1280 fn lifecycle_independent_of_assignment() {
1281 let r = VnodeRegistry::new(4);
1284 r.mark_restoring(&[2]);
1285 r.set_assignment(vec![NodeId(1), NodeId(1), NodeId(1), NodeId(1)].into());
1286 assert!(r.is_restoring(2));
1287 }
1288
1289 fn node(id: u64, region: &str, zone: &str, rack: &str) -> (NodeId, Locality) {
1293 (
1294 NodeId(id),
1295 Locality::new(vec![region.into(), zone.into(), rack.into()]),
1296 )
1297 }
1298
1299 const TIER_ZONE: usize = 1;
1300
1301 #[test]
1302 fn locality_parse_and_domain_at() {
1303 let l = Locality::parse("region=us-east-1;zone=us-east-1a;rack=r17");
1304 assert_eq!(l.domain_at(0), "us-east-1");
1305 assert_eq!(l.domain_at(1), "us-east-1;us-east-1a");
1306 assert_eq!(l.domain_at(2), "us-east-1;us-east-1a;r17");
1307 assert_eq!(l.domain_at(99), "us-east-1;us-east-1a;r17"); assert_eq!(Locality::parse("rack17").domain_at(0), "rack17"); assert_eq!(Locality::parse("").domain_at(0), ""); }
1311
1312 #[test]
1313 fn owners_per_domain_counts_by_zone() {
1314 let nodes = vec![node(1, "r", "z1", "a"), node(2, "r", "z2", "a")];
1315 let owners = [NodeId(1), NodeId(1), NodeId(2), NodeId::UNASSIGNED];
1317 let counts = owners_per_domain(&owners, &nodes, TIER_ZONE);
1318 assert_eq!(counts["r;z1"], 2);
1319 assert_eq!(counts["r;z2"], 1);
1320 assert_eq!(counts[""], 1);
1321 }
1322}