1use super::{
50 ControlWaker, CoordinationError, CoordinationErrorKind, CoordinationEvent, LeaseEpoch,
51 SplitCoordinator, SplitId, SplitPlanner, SplitProgress, SplitSpec,
52};
53use crate::error::{ErrorClass, SourceError};
54use crate::record::PartitionId;
55use crate::source::{DrainBarrier, LaneId, SourceEvent, SourceLane};
56use std::collections::BTreeMap;
57use std::fmt;
58use std::time::Duration;
59
60#[derive(Debug)]
64#[non_exhaustive]
65pub struct SplitOpening<'a> {
66 pub split: &'a SplitSpec,
68 pub resume: Option<&'a SplitProgress>,
71 pub lane: LaneId,
74 pub partition: PartitionId,
77 pub epoch: LeaseEpoch,
79 pub waker: &'a ControlWaker,
84}
85
86pub trait SplitSource {
93 type Lane: SourceLane;
95
96 fn open_split(&mut self, opening: SplitOpening<'_>) -> Result<Self::Lane, SourceError>;
99
100 fn validate_resume(
105 &self,
106 split: &SplitSpec,
107 progress: &SplitProgress,
108 ) -> Result<(), SourceError> {
109 let _ = (split, progress);
110 Ok(())
111 }
112
113 fn encode_commit(
118 &mut self,
119 split: &SplitId,
120 watermark: i64,
121 ) -> Result<SplitProgress, SourceError>;
122
123 fn sweep(&mut self, split: &SplitId) -> Result<Option<SplitProgress>, SourceError>;
128
129 fn close_split(&mut self, split: &SplitId);
139
140 fn take_finishing(&mut self) -> Vec<SplitId> {
148 Vec::new()
149 }
150
151 fn begin_revoke(&mut self, split: &SplitId) -> bool {
183 let _ = split;
184 false
185 }
186
187 fn drain_ready(&mut self, split: &SplitId) -> Result<Option<SplitProgress>, SourceError> {
207 let _ = split;
208 Ok(None)
209 }
210}
211
212#[derive(Debug, PartialEq, Eq, Clone, Copy)]
213enum TenancyState {
214 Live,
216 Draining,
224 Retired,
228}
229
230#[derive(Debug)]
231struct Tenancy {
232 split: SplitSpec,
233 epoch: LeaseEpoch,
234 lane: Option<LaneId>,
235 state: TenancyState,
236 fenced: bool,
238 progress: Option<SplitProgress>,
243 completed: bool,
245 handed_off: bool,
250}
251
252pub struct CoordinationDriver {
255 coordinator: Box<dyn SplitCoordinator>,
256 wait: crossbeam_channel::Receiver<()>,
259 waker: ControlWaker,
260 tenancies: BTreeMap<PartitionId, Tenancy>,
261 by_split: BTreeMap<SplitId, PartitionId>,
262 pending_lost: Vec<LaneId>,
264 pending_retired: Vec<LaneId>,
267 pending_open: Vec<PartitionId>,
270 all_complete: bool,
271 stalled: Option<(u64, u64)>,
272 stall_drains: bool,
273 started: bool,
274 next_partition: u32,
275 next_lane: u32,
277}
278
279impl fmt::Debug for CoordinationDriver {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 f.debug_struct("CoordinationDriver")
282 .field("tenancies", &self.tenancies.len())
283 .field("live", &self.by_split.len())
284 .field("pending_lost", &self.pending_lost.len())
285 .field("pending_retired", &self.pending_retired.len())
286 .field("pending_open", &self.pending_open.len())
287 .field("all_complete", &self.all_complete)
288 .field("stalled", &self.stalled)
289 .field("started", &self.started)
290 .finish_non_exhaustive()
291 }
292}
293
294impl CoordinationDriver {
295 #[must_use]
297 pub fn new(mut coordinator: Box<dyn SplitCoordinator>) -> CoordinationDriver {
298 let (waker, wait) = super::control_channel();
299 coordinator.set_waker(waker.clone());
300 CoordinationDriver {
301 coordinator,
302 wait,
303 waker,
304 tenancies: BTreeMap::new(),
305 by_split: BTreeMap::new(),
306 pending_lost: Vec::new(),
307 pending_retired: Vec::new(),
308 pending_open: Vec::new(),
309 all_complete: false,
310 stalled: None,
311 stall_drains: false,
312 started: false,
313 next_partition: 0,
314 next_lane: 0,
315 }
316 }
317
318 #[must_use]
323 pub fn stall_drains(mut self, drains: bool) -> CoordinationDriver {
324 self.stall_drains = drains;
325 self
326 }
327
328 pub fn start<L>(
333 &mut self,
334 planner: Box<dyn SplitPlanner>,
335 ) -> Result<SourceEvent<L>, SourceError> {
336 assert!(!self.started, "CoordinationDriver::start called twice");
337 self.coordinator.start(planner).map_err(as_source_error)?;
338 self.started = true;
339 Ok(SourceEvent::LanesAssigned(Vec::new()))
340 }
341
342 pub fn poll_events<S: SplitSource>(
345 &mut self,
346 source: &mut S,
347 timeout: Duration,
348 ) -> Result<SourceEvent<S::Lane>, SourceError> {
349 assert!(self.started, "poll_events before start");
350
351 let mut park = timeout;
358 loop {
359 if !self.pending_lost.is_empty() {
361 let lanes = std::mem::take(&mut self.pending_lost);
362 let barrier = DrainBarrier::new(lanes.len());
363 return Ok(SourceEvent::LanesRevoked { lanes, barrier });
364 }
365
366 if !self.pending_retired.is_empty() {
369 let lanes = std::mem::take(&mut self.pending_retired);
370 return Ok(SourceEvent::LanesRetired { lanes });
371 }
372
373 self.tenancies
381 .retain(|_, t| t.state != TenancyState::Retired);
382
383 if !self.pending_open.is_empty() {
387 let lanes = self.open_pending(source)?;
388 if !lanes.is_empty() {
389 return Ok(SourceEvent::LanesAdded(lanes));
390 }
391 }
392
393 if let Some((completed, quarantined)) = self.stalled {
395 if self.stall_drains {
396 tracing::warn!(
397 completed,
398 quarantined,
399 "job stalled; draining as configured"
400 );
401 return Ok(SourceEvent::Drained);
402 }
403 return Err(SourceError::Client {
404 class: ErrorClass::Fatal,
405 reason: format!(
406 "coordinated job stalled: {completed} splits completed but {quarantined} \
407 are quarantined and out of delivery attempts; inspect \
408 spate_coordination_splits_quarantined and requeue or exclude them"
409 ),
410 });
411 }
412 if self.all_complete {
413 return Ok(SourceEvent::Drained);
414 }
415
416 let events = self.coordinator.poll().map_err(as_source_error)?;
419 for event in events {
420 self.apply(source, event)?;
421 }
422
423 self.sweep(source)?;
425
426 self.advance_drains(source)?;
431
432 if !self.pending_lost.is_empty()
433 || !self.pending_retired.is_empty()
434 || !self.pending_open.is_empty()
435 || self.all_complete
436 || self.stalled.is_some()
437 {
438 park = Duration::ZERO;
441 continue;
442 }
443 break;
444 }
445
446 let finishing = source.take_finishing();
449 if !finishing.is_empty() {
450 let partitions: Vec<PartitionId> = finishing
451 .iter()
452 .filter_map(|split| self.by_split.get(split).copied())
453 .collect();
454 if !partitions.is_empty() {
455 return Ok(SourceEvent::CommitReady { partitions });
456 }
457 }
458
459 if !park.is_zero() {
466 let _ = self.wait.recv_timeout(park);
467 }
468 Ok(SourceEvent::Idle)
469 }
470
471 pub fn commit<S: SplitSource>(
474 &mut self,
475 source: &mut S,
476 watermarks: &[(PartitionId, i64)],
477 ) -> Result<(), SourceError> {
478 for &(partition, watermark) in watermarks {
479 let Some(tenancy) = self.tenancies.get(&partition) else {
480 continue;
484 };
485 if tenancy.state == TenancyState::Retired || tenancy.fenced || tenancy.completed {
486 continue;
487 }
488 let split = tenancy.split.id.clone();
489 let progress = source.encode_commit(&split, watermark)?;
490 self.commit_progress(source, partition, &split, progress)?;
491 }
492 Ok(())
493 }
494
495 pub fn fail<S: SplitSource>(
499 &mut self,
500 source: &mut S,
501 split: &SplitId,
502 reason: &str,
503 ) -> Result<(), SourceError> {
504 let Some(&partition) = self.by_split.get(split) else {
505 return Ok(()); };
507 match self.coordinator.fail(split, reason) {
508 Ok(()) => {}
509 Err(e) if e.kind == CoordinationErrorKind::Fenced => {}
511 Err(e) => return Err(as_source_error(e)),
512 }
513 self.retire(source, partition, false);
514 Ok(())
515 }
516
517 pub fn release(&mut self) {
520 if !self.started {
521 return;
522 }
523 let held: Vec<SplitId> = self.by_split.keys().cloned().collect();
524 if held.is_empty() {
525 return;
526 }
527 if let Err(e) = self.coordinator.release(&held) {
528 tracing::warn!(error = %e, "graceful split release failed; leases will expire");
529 }
530 }
531
532 #[must_use]
534 pub fn assignments(&self) -> Vec<(SplitId, LaneId)> {
535 self.by_split
536 .iter()
537 .filter_map(|(split, partition)| {
538 let lane = self.tenancies.get(partition)?.lane?;
539 Some((split.clone(), lane))
540 })
541 .collect()
542 }
543
544 fn apply<S: SplitSource>(
545 &mut self,
546 source: &mut S,
547 event: CoordinationEvent,
548 ) -> Result<(), SourceError> {
549 match event {
550 CoordinationEvent::Gained {
551 split,
552 epoch,
553 progress,
554 } => {
555 if let Some(&stale) = self.by_split.get(&split.id) {
556 tracing::warn!(split = %split.id, "gained a split already held; retiring stale tenancy");
559 self.retire(source, stale, false);
560 }
561 if let Some(progress) = progress.as_ref() {
562 source.validate_resume(&split, progress)?;
563 }
564 let partition = PartitionId(self.next_partition);
565 self.next_partition += 1;
566 self.by_split.insert(split.id.clone(), partition);
567 self.tenancies.insert(
568 partition,
569 Tenancy {
570 split,
571 epoch,
572 lane: None,
573 state: TenancyState::Live,
574 fenced: false,
575 progress,
576 completed: false,
577 handed_off: false,
578 },
579 );
580 self.pending_open.push(partition);
581 }
582 CoordinationEvent::RevokeRequested { split } => {
583 if let Some(&partition) = self.by_split.get(&split)
603 && self
604 .tenancies
605 .get(&partition)
606 .is_some_and(|t| t.state == TenancyState::Draining && !t.fenced)
607 {
608 return Ok(());
609 }
610 let accepted = match self.by_split.get(&split) {
611 Some(&partition) => {
612 let eligible = self.tenancies.get(&partition).is_some_and(|t| {
613 t.state == TenancyState::Live
614 && t.lane.is_some()
615 && !t.fenced
616 && !t.completed
617 });
618 if eligible && source.begin_revoke(&split) {
619 if let Some(t) = self.tenancies.get_mut(&partition) {
622 t.state = TenancyState::Draining;
623 }
624 true
625 } else {
626 false
627 }
628 }
629 None => false,
630 };
631 if !accepted && let Err(e) = self.coordinator.decline_revoke(&split) {
632 tracing::warn!(split = %split, error = %e, "revocation decline failed");
635 }
636 }
637 CoordinationEvent::Lost { split } => {
638 if let Some(&partition) = self.by_split.get(&split) {
639 self.retire(source, partition, false);
640 }
641 }
643 CoordinationEvent::Quarantined { split, attempts } => {
644 tracing::warn!(split = %split, attempts, "split quarantined");
645 if let Some(&partition) = self.by_split.get(&split) {
646 self.retire(source, partition, false);
647 }
648 }
649 CoordinationEvent::AllComplete => {
650 if self.next_partition == 0 {
657 tracing::info!(
658 "coordinated job completed without this instance holding any split — \
659 the job finished before this instance's first rebalance window, or \
660 the fleet has more replicas than splits (see the scaling-out guide)"
661 );
662 }
663 self.all_complete = true;
664 }
665 CoordinationEvent::Stalled {
666 completed,
667 quarantined,
668 } => {
669 self.stalled = Some((completed, quarantined));
670 }
671 }
672 Ok(())
673 }
674
675 fn retire<S: SplitSource>(&mut self, source: &mut S, partition: PartitionId, fenced: bool) {
678 let Some(tenancy) = self.tenancies.get_mut(&partition) else {
679 return;
680 };
681 if tenancy.state == TenancyState::Retired {
682 if fenced {
683 tenancy.fenced = true;
684 }
685 return;
686 }
687 tenancy.state = TenancyState::Retired;
688 tenancy.fenced |= fenced;
689 self.by_split.remove(&tenancy.split.id);
690 let split = tenancy.split.id.clone();
691 if let Some(lane) = tenancy.lane.take() {
692 if tenancy.completed || tenancy.handed_off {
693 self.pending_retired.push(lane);
697 } else {
698 self.pending_lost.push(lane);
699 }
700 }
701 source.close_split(&split);
702 }
703
704 fn open_pending<S: SplitSource>(
716 &mut self,
717 source: &mut S,
718 ) -> Result<Vec<S::Lane>, SourceError> {
719 let staged = std::mem::take(&mut self.pending_open);
720 let mut lanes = Vec::with_capacity(staged.len());
721 let mut opened: Vec<PartitionId> = Vec::new();
723 for idx in 0..staged.len() {
724 let partition = staged[idx];
725 let Some(tenancy) = self.tenancies.get_mut(&partition) else {
726 continue; };
728 if tenancy.state != TenancyState::Live || tenancy.lane.is_some() {
729 continue;
730 }
731 let lane_id = LaneId(self.next_lane);
732 self.next_lane = self
733 .next_lane
734 .checked_add(1)
735 .expect("lane ids exhausted (u32)");
736 tenancy.lane = Some(lane_id);
737 let opening = SplitOpening {
738 split: &tenancy.split,
739 resume: tenancy.progress.as_ref(),
740 lane: lane_id,
741 partition,
742 epoch: tenancy.epoch,
743 waker: &self.waker,
744 };
745 match source.open_split(opening) {
746 Ok(lane) => {
747 lanes.push(lane);
748 opened.push(partition);
749 }
750 Err(e) => {
751 drop(lanes);
755 for p in opened.iter().chain(std::iter::once(&partition)) {
756 if let Some(t) = self.tenancies.get_mut(p) {
757 t.lane = None;
758 }
759 }
760 self.pending_open = staged;
761 return Err(e);
762 }
763 }
764 }
765 Ok(lanes)
766 }
767
768 fn sweep<S: SplitSource>(&mut self, source: &mut S) -> Result<(), SourceError> {
769 let candidates: Vec<PartitionId> = self
770 .tenancies
771 .iter()
772 .filter(|(_, t)| t.state == TenancyState::Live && !t.fenced && !t.completed)
773 .map(|(&p, _)| p)
774 .collect();
775 for partition in candidates {
776 let split = self.tenancies[&partition].split.id.clone();
777 if let Some(progress) = source.sweep(&split)? {
778 self.commit_progress(source, partition, &split, progress)?;
779 }
780 }
781 Ok(())
782 }
783
784 fn advance_drains<S: SplitSource>(&mut self, source: &mut S) -> Result<(), SourceError> {
797 let draining: Vec<PartitionId> = self
798 .tenancies
799 .iter()
800 .filter(|(_, t)| t.state == TenancyState::Draining && !t.fenced)
801 .map(|(&p, _)| p)
802 .collect();
803 for partition in draining {
804 let split = self.tenancies[&partition].split.id.clone();
805 let Some(progress) = source.drain_ready(&split)? else {
806 continue; };
808 debug_assert!(
809 !progress.completed,
810 "a revocation commit hands the split off, it must not complete it"
811 );
812 self.commit_drained(source, partition, &split, progress)?;
813 }
814 Ok(())
815 }
816
817 fn try_commit<S: SplitSource>(
822 &mut self,
823 source: &mut S,
824 partition: PartitionId,
825 split: &SplitId,
826 progress: &SplitProgress,
827 ) -> Result<CommitDisposition, SourceError> {
828 match self.coordinator.commit(split, progress) {
829 Ok(()) => Ok(CommitDisposition::Durable),
830 Err(e) if e.kind == CoordinationErrorKind::Fenced => {
831 tracing::warn!(split = %split, "commit fenced; split lost to a peer");
836 self.retire(source, partition, true);
837 Ok(CommitDisposition::Fenced)
838 }
839 Err(e) if e.kind == CoordinationErrorKind::Retryable => {
840 tracing::warn!(split = %split, error = %e, "commit deferred; will retry");
843 Ok(CommitDisposition::Deferred)
844 }
845 Err(e) => Err(as_source_error(e)),
846 }
847 }
848
849 fn commit_progress<S: SplitSource>(
851 &mut self,
852 source: &mut S,
853 partition: PartitionId,
854 split: &SplitId,
855 progress: SplitProgress,
856 ) -> Result<(), SourceError> {
857 let progress = if progress.completed
864 && self
865 .tenancies
866 .get(&partition)
867 .is_some_and(|t| t.state == TenancyState::Draining)
868 {
869 tracing::error!(
870 split = %split,
871 "source reported a handing-off split completed; forcing \
872 completed=false — a drain cut is never terminal"
873 );
874 SplitProgress::new(progress.watermark, progress.state)
875 } else {
876 progress
877 };
878 match self.try_commit(source, partition, split, &progress)? {
879 CommitDisposition::Durable => {
880 let tenancy = self.tenancies.get_mut(&partition).expect("live tenancy");
881 let completed = progress.completed;
882 tenancy.progress = Some(progress);
883 if completed {
884 tenancy.completed = true;
885 self.retire(source, partition, false);
887 }
888 }
889 CommitDisposition::Fenced => {}
891 CommitDisposition::Deferred => {
892 let tenancy = self.tenancies.get_mut(&partition).expect("live tenancy");
895 tenancy.progress = Some(progress);
896 }
897 }
898 Ok(())
899 }
900
901 fn commit_drained<S: SplitSource>(
906 &mut self,
907 source: &mut S,
908 partition: PartitionId,
909 split: &SplitId,
910 progress: SplitProgress,
911 ) -> Result<(), SourceError> {
912 let progress = if progress.completed {
915 tracing::error!(
916 split = %split,
917 "drain_ready returned completed=true; forcing completed=false — \
918 a drain cut is never terminal"
919 );
920 SplitProgress::new(progress.watermark, progress.state)
921 } else {
922 progress
923 };
924 match self.try_commit(source, partition, split, &progress)? {
925 CommitDisposition::Durable => {
926 let tenancy = self
931 .tenancies
932 .get_mut(&partition)
933 .expect("handing-off tenancy");
934 tenancy.progress = Some(progress);
935 tenancy.handed_off = true;
936 if let Err(e) = self
937 .coordinator
938 .release_drained(std::slice::from_ref(split))
939 {
940 tracing::warn!(
944 split = %split,
945 error = %e,
946 "drain release failed; lease will expire and a peer will take over"
947 );
948 }
949 self.retire(source, partition, false);
950 }
951 CommitDisposition::Fenced => {}
955 CommitDisposition::Deferred => {
956 let tenancy = self
960 .tenancies
961 .get_mut(&partition)
962 .expect("handing-off tenancy");
963 tenancy.progress = Some(progress);
964 }
965 }
966 Ok(())
967 }
968}
969
970enum CommitDisposition {
973 Durable,
975 Fenced,
978 Deferred,
981}
982
983fn as_source_error(e: CoordinationError) -> SourceError {
984 SourceError::Client {
985 class: e.class(),
986 reason: e.to_string(),
987 }
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993 use crate::checkpoint::AckRef;
994 use crate::coordination::{PlanContext, PlanFinality, SplitPlan};
995 use crate::record::RawPayload;
996 use crate::source::PayloadBatch;
997 use std::cell::RefCell;
998 use std::collections::{HashMap, HashSet, VecDeque};
999 use std::rc::Rc;
1000 use std::sync::{Arc, Mutex};
1001 use std::time::Instant;
1002
1003 #[derive(Default)]
1007 struct ScriptState {
1008 batches: VecDeque<Vec<CoordinationEvent>>,
1009 commit_outcomes: HashMap<String, VecDeque<CoordinationErrorKind>>,
1010 commits: Vec<(SplitId, SplitProgress)>,
1011 fails: Vec<(SplitId, String)>,
1012 released: Vec<SplitId>,
1013 released_drained: Vec<SplitId>,
1016 declined: Vec<SplitId>,
1019 started: bool,
1020 waker: Option<ControlWaker>,
1021 }
1022
1023 #[derive(Clone, Default)]
1024 struct Script(Arc<Mutex<ScriptState>>);
1025
1026 impl Script {
1027 fn push(&self, events: Vec<CoordinationEvent>) {
1028 let mut st = self.0.lock().unwrap();
1029 st.batches.push_back(events);
1030 if let Some(w) = &st.waker {
1031 w.wake();
1032 }
1033 }
1034
1035 fn fail_next_commit(&self, split: &str, kind: CoordinationErrorKind) {
1036 self.0
1037 .lock()
1038 .unwrap()
1039 .commit_outcomes
1040 .entry(split.to_string())
1041 .or_default()
1042 .push_back(kind);
1043 }
1044
1045 fn commits(&self) -> Vec<(SplitId, SplitProgress)> {
1046 self.0.lock().unwrap().commits.clone()
1047 }
1048
1049 fn released(&self) -> Vec<SplitId> {
1050 self.0.lock().unwrap().released.clone()
1051 }
1052
1053 fn released_drained(&self) -> Vec<SplitId> {
1054 self.0.lock().unwrap().released_drained.clone()
1055 }
1056
1057 fn declined(&self) -> Vec<SplitId> {
1058 self.0.lock().unwrap().declined.clone()
1059 }
1060
1061 fn fails(&self) -> Vec<(SplitId, String)> {
1062 self.0.lock().unwrap().fails.clone()
1063 }
1064 }
1065
1066 struct ScriptedCoordinator(Script);
1067
1068 impl SplitCoordinator for ScriptedCoordinator {
1069 fn start(&mut self, _planner: Box<dyn SplitPlanner>) -> Result<(), CoordinationError> {
1070 self.0.0.lock().unwrap().started = true;
1071 Ok(())
1072 }
1073
1074 fn set_waker(&mut self, waker: ControlWaker) {
1075 self.0.0.lock().unwrap().waker = Some(waker);
1076 }
1077
1078 fn poll(&mut self) -> Result<Vec<CoordinationEvent>, CoordinationError> {
1079 Ok(self
1080 .0
1081 .0
1082 .lock()
1083 .unwrap()
1084 .batches
1085 .pop_front()
1086 .unwrap_or_default())
1087 }
1088
1089 fn commit(
1090 &mut self,
1091 split: &SplitId,
1092 progress: &SplitProgress,
1093 ) -> Result<(), CoordinationError> {
1094 let mut s = self.0.0.lock().unwrap();
1095 if let Some(kinds) = s.commit_outcomes.get_mut(split.as_str())
1096 && let Some(kind) = kinds.pop_front()
1097 {
1098 return Err(CoordinationError::new(kind, "scripted"));
1099 }
1100 s.commits.push((split.clone(), progress.clone()));
1101 Ok(())
1102 }
1103
1104 fn fail(&mut self, split: &SplitId, reason: &str) -> Result<(), CoordinationError> {
1105 self.0
1106 .0
1107 .lock()
1108 .unwrap()
1109 .fails
1110 .push((split.clone(), reason.to_string()));
1111 Ok(())
1112 }
1113
1114 fn release(&mut self, splits: &[SplitId]) -> Result<(), CoordinationError> {
1115 self.0
1116 .0
1117 .lock()
1118 .unwrap()
1119 .released
1120 .extend(splits.iter().cloned());
1121 Ok(())
1122 }
1123
1124 fn release_drained(&mut self, splits: &[SplitId]) -> Result<(), CoordinationError> {
1125 self.0
1126 .0
1127 .lock()
1128 .unwrap()
1129 .released_drained
1130 .extend(splits.iter().cloned());
1131 Ok(())
1132 }
1133
1134 fn decline_revoke(&mut self, split: &SplitId) -> Result<(), CoordinationError> {
1135 self.0.0.lock().unwrap().declined.push(split.clone());
1136 Ok(())
1137 }
1138 }
1139
1140 struct NoopPlanner;
1141
1142 impl SplitPlanner for NoopPlanner {
1143 fn fingerprint(&self) -> String {
1144 "test:v1".into()
1145 }
1146
1147 fn plan(&mut self, _ctx: PlanContext<'_>) -> Result<SplitPlan, CoordinationError> {
1148 Ok(SplitPlan::new(vec![], PlanFinality::Final))
1149 }
1150 }
1151
1152 enum NoBatch {}
1156
1157 impl<'buf> PayloadBatch<'buf> for NoBatch {
1158 fn next_payload(&mut self) -> Option<RawPayload<'buf>> {
1159 match *self {}
1160 }
1161
1162 fn ack(&self) -> &AckRef {
1163 match *self {}
1164 }
1165 }
1166
1167 #[derive(Debug)]
1168 struct StubLane {
1169 lane: LaneId,
1170 partition: PartitionId,
1171 }
1172
1173 impl SourceLane for StubLane {
1174 type Batch<'a> = NoBatch;
1175
1176 fn id(&self) -> LaneId {
1177 self.lane
1178 }
1179
1180 fn partition(&self) -> PartitionId {
1181 self.partition
1182 }
1183
1184 fn poll(
1185 &mut self,
1186 _max: usize,
1187 _timeout: Duration,
1188 ) -> Result<Option<NoBatch>, SourceError> {
1189 Ok(None)
1190 }
1191 }
1192
1193 #[derive(Default)]
1196 struct TestSource {
1197 opened: Vec<(String, Option<i64>, LaneId, PartitionId, u64)>,
1198 closed: Vec<String>,
1199 encoded: Vec<(String, i64)>,
1200 sweeps: Rc<RefCell<HashMap<String, SplitProgress>>>,
1201 complete_at: HashMap<String, i64>,
1202 reject_resume: bool,
1203 finishing: Vec<String>,
1204 fail_open: Vec<String>,
1207 accept_revoke: HashSet<String>,
1210 begin_revoke_calls: Vec<String>,
1212 drain_ready_calls: Vec<String>,
1215 ready_progress: Rc<RefCell<HashMap<String, SplitProgress>>>,
1219 }
1220
1221 impl SplitSource for TestSource {
1222 type Lane = StubLane;
1223
1224 fn open_split(&mut self, o: SplitOpening<'_>) -> Result<StubLane, SourceError> {
1225 let id = o.split.id.as_str().to_string();
1226 if let Some(i) = self.fail_open.iter().position(|s| *s == id) {
1227 self.fail_open.remove(i);
1228 return Err(SourceError::Client {
1229 class: ErrorClass::Retryable,
1230 reason: format!("open_split failed for {id}"),
1231 });
1232 }
1233 self.opened.push((
1234 o.split.id.as_str().to_string(),
1235 o.resume.map(|p| p.watermark),
1236 o.lane,
1237 o.partition,
1238 o.epoch.0,
1239 ));
1240 Ok(StubLane {
1241 lane: o.lane,
1242 partition: o.partition,
1243 })
1244 }
1245
1246 fn validate_resume(
1247 &self,
1248 split: &SplitSpec,
1249 _progress: &SplitProgress,
1250 ) -> Result<(), SourceError> {
1251 if self.reject_resume {
1252 return Err(SourceError::Client {
1253 class: ErrorClass::Fatal,
1254 reason: format!("resume drift on {}", split.id),
1255 });
1256 }
1257 Ok(())
1258 }
1259
1260 fn encode_commit(
1261 &mut self,
1262 split: &SplitId,
1263 watermark: i64,
1264 ) -> Result<SplitProgress, SourceError> {
1265 self.encoded.push((split.as_str().to_string(), watermark));
1266 let completed = self.complete_at.get(split.as_str()) == Some(&watermark);
1267 Ok(if completed {
1268 SplitProgress::completed(watermark, vec![])
1269 } else {
1270 SplitProgress::new(watermark, vec![])
1271 })
1272 }
1273
1274 fn sweep(&mut self, split: &SplitId) -> Result<Option<SplitProgress>, SourceError> {
1275 Ok(self.sweeps.borrow_mut().remove(split.as_str()))
1276 }
1277
1278 fn close_split(&mut self, split: &SplitId) {
1279 self.closed.push(split.as_str().to_string());
1280 }
1281
1282 fn take_finishing(&mut self) -> Vec<SplitId> {
1283 std::mem::take(&mut self.finishing)
1284 .into_iter()
1285 .map(|s| SplitId::new(&s).unwrap())
1286 .collect()
1287 }
1288
1289 fn begin_revoke(&mut self, split: &SplitId) -> bool {
1290 self.begin_revoke_calls.push(split.as_str().to_string());
1291 self.accept_revoke.contains(split.as_str())
1292 }
1293
1294 fn drain_ready(&mut self, split: &SplitId) -> Result<Option<SplitProgress>, SourceError> {
1295 self.drain_ready_calls.push(split.as_str().to_string());
1296 Ok(self.ready_progress.borrow().get(split.as_str()).cloned())
1297 }
1298 }
1299
1300 fn split(id: &str) -> SplitSpec {
1304 SplitSpec::new(SplitId::new(id).unwrap(), format!("desc:{id}").into_bytes())
1305 }
1306
1307 fn gained(id: &str, epoch: u64, watermark: Option<i64>) -> CoordinationEvent {
1308 CoordinationEvent::Gained {
1309 split: split(id),
1310 epoch: LeaseEpoch(epoch),
1311 progress: watermark.map(|w| SplitProgress::new(w, vec![])),
1312 }
1313 }
1314
1315 fn driver(script: &Script) -> CoordinationDriver {
1316 let mut d = CoordinationDriver::new(Box::new(ScriptedCoordinator(script.clone())));
1317 let ready: SourceEvent<StubLane> = d.start(Box::new(NoopPlanner)).unwrap();
1318 assert!(
1319 matches!(ready, SourceEvent::LanesAssigned(ref lanes) if lanes.is_empty()),
1320 "start must return the empty ready signal"
1321 );
1322 d
1323 }
1324
1325 fn poll(d: &mut CoordinationDriver, s: &mut TestSource) -> SourceEvent<StubLane> {
1326 d.poll_events(s, Duration::ZERO).unwrap()
1327 }
1328
1329 #[test]
1333 fn a_signal_cuts_the_control_plane_park_short() {
1334 let script = Script::default();
1341 let mut d = driver(&script);
1342 let mut s = TestSource::default();
1343 let park = Duration::from_millis(400);
1344
1345 let t0 = Instant::now();
1349 assert!(matches!(
1350 d.poll_events(&mut s, park).unwrap(),
1351 SourceEvent::Idle
1352 ));
1353 let idle = t0.elapsed();
1354 assert!(
1355 idle >= park / 2,
1356 "expected a real park, returned after {idle:?}"
1357 );
1358
1359 let signaller = script.clone();
1363 let handle = std::thread::spawn(move || {
1364 std::thread::sleep(Duration::from_millis(20));
1365 signaller.push(vec![CoordinationEvent::AllComplete]);
1366 });
1367 let t1 = Instant::now();
1368 let _ = d.poll_events(&mut s, park).unwrap();
1369 let woken = t1.elapsed();
1370 handle.join().unwrap();
1371 assert!(
1372 woken < park / 2,
1373 "a signal must cut the park short, but it ran {woken:?} of {park:?}"
1374 );
1375 assert!(matches!(
1376 d.poll_events(&mut s, Duration::ZERO).unwrap(),
1377 SourceEvent::Drained
1378 ));
1379 }
1380
1381 #[test]
1382 fn a_failed_open_undoes_the_whole_batch_instead_of_stranding_lanes() {
1383 let script = Script::default();
1389 let mut d = driver(&script);
1390 let mut s = TestSource {
1391 fail_open: vec!["b".into()],
1392 ..TestSource::default()
1393 };
1394
1395 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
1396 let err = d
1397 .poll_events(&mut s, Duration::ZERO)
1398 .expect_err("the failing open must surface");
1399 assert!(err.to_string().contains("open_split failed for b"), "{err}");
1400 assert_eq!(s.opened.len(), 1, "a opened before b failed");
1401
1402 let event = poll(&mut d, &mut s);
1404 let SourceEvent::LanesAdded(lanes) = event else {
1405 panic!("expected both lanes after the retry, got {event:?}");
1406 };
1407 assert_eq!(lanes.len(), 2);
1408 let reopened: Vec<&str> = s.opened.iter().map(|o| o.0.as_str()).collect();
1409 assert_eq!(reopened, ["a", "a", "b"], "a re-opens on the retry");
1410 assert_eq!(lanes[0].id(), LaneId(2));
1412 assert_eq!(lanes[1].id(), LaneId(3));
1413 }
1414
1415 #[test]
1416 fn gains_coalesce_into_one_added_batch() {
1417 let script = Script::default();
1418 let mut d = driver(&script);
1419 let mut s = TestSource::default();
1420
1421 script.push(vec![gained("b", 1, Some(7)), gained("a", 1, None)]);
1422 let event = poll(&mut d, &mut s);
1423 let SourceEvent::LanesAdded(lanes) = event else {
1424 panic!("expected added lanes, got {event:?}");
1425 };
1426 assert_eq!(lanes.len(), 2);
1427 assert_eq!(s.opened[0].0, "b");
1429 assert_eq!(s.opened[0].2, LaneId(0));
1430 assert_eq!(s.opened[0].1, Some(7), "carried progress reaches open");
1431 assert_eq!(s.opened[1].0, "a");
1432 assert_eq!(s.opened[1].2, LaneId(1));
1433 assert_ne!(s.opened[0].3, s.opened[1].3);
1434 assert_eq!(d.assignments().len(), 2);
1435 }
1436
1437 #[test]
1438 fn a_mid_flow_gain_never_touches_live_lanes_and_their_commits_fold() {
1439 let script = Script::default();
1440 let mut d = driver(&script);
1441 let mut s = TestSource::default();
1442 script.push(vec![gained("a", 1, None)]);
1443 poll(&mut d, &mut s);
1444 let a_partition = s.opened[0].3;
1445
1446 script.push(vec![gained("b", 1, None)]);
1448 let event = poll(&mut d, &mut s);
1449 let SourceEvent::LanesAdded(lanes) = event else {
1450 panic!("expected added lanes, got {event:?}");
1451 };
1452 assert_eq!(lanes.len(), 1, "only the new split's lane");
1453 assert!(
1454 s.closed.is_empty(),
1455 "a routine gain must never detach flowing fetchers"
1456 );
1457
1458 d.commit(&mut s, &[(a_partition, 42)]).unwrap();
1461 assert_eq!(s.encoded, vec![("a".to_string(), 42)]);
1462 assert_eq!(script.commits().len(), 1);
1463 assert_eq!(script.commits()[0].0.as_str(), "a");
1464 assert!(
1466 d.assignments()
1467 .contains(&(SplitId::new("a").unwrap(), LaneId(0)))
1468 );
1469 }
1470
1471 #[test]
1472 fn finishing_splits_surface_as_commit_ready_once() {
1473 let script = Script::default();
1474 let mut d = driver(&script);
1475 let mut s = TestSource::default();
1476 script.push(vec![gained("a", 1, None)]);
1477 poll(&mut d, &mut s);
1478 let a_partition = s.opened[0].3;
1479
1480 s.finishing.push("a".to_string());
1481 let event = poll(&mut d, &mut s);
1482 let SourceEvent::CommitReady { partitions } = event else {
1483 panic!("expected commit-ready, got {event:?}");
1484 };
1485 assert_eq!(partitions, vec![a_partition]);
1486 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
1488 }
1489
1490 #[test]
1491 fn loss_surfaces_as_partial_revoke_and_detaches_fetchers() {
1492 let script = Script::default();
1493 let mut d = driver(&script);
1494 let mut s = TestSource::default();
1495 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
1496 poll(&mut d, &mut s);
1497
1498 script.push(vec![CoordinationEvent::Lost {
1499 split: SplitId::new("a").unwrap(),
1500 }]);
1501 let event = poll(&mut d, &mut s);
1502 let SourceEvent::LanesRevoked { lanes, barrier } = event else {
1503 panic!("expected revoke, got {event:?}");
1504 };
1505 assert_eq!(lanes, vec![LaneId(0)]);
1506 assert_eq!(barrier.remaining(), 1, "one party per revoked lane");
1507 assert_eq!(s.closed, vec!["a"], "fetcher detached on loss");
1508 assert_eq!(d.assignments().len(), 1);
1509 }
1510
1511 #[test]
1512 fn late_drain_commit_after_loss_is_skipped() {
1513 let script = Script::default();
1514 let mut d = driver(&script);
1515 let mut s = TestSource::default();
1516 script.push(vec![gained("a", 1, None)]);
1517 poll(&mut d, &mut s);
1518 let partition = s.opened[0].3;
1519
1520 script.push(vec![CoordinationEvent::Lost {
1521 split: SplitId::new("a").unwrap(),
1522 }]);
1523 poll(&mut d, &mut s);
1524
1525 d.commit(&mut s, &[(partition, 42)]).unwrap();
1527 assert!(s.encoded.is_empty(), "retired tenancy must not encode");
1528 assert!(script.commits().is_empty(), "and must not commit");
1529 }
1530
1531 #[test]
1532 fn fenced_commit_quarantines_the_tenancy_and_never_respawns_it() {
1533 let script = Script::default();
1534 let mut d = driver(&script);
1535 let mut s = TestSource::default();
1536 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
1537 poll(&mut d, &mut s);
1538 let a_partition = s.opened[0].3;
1539 let b_partition = s.opened[1].3;
1540
1541 script.fail_next_commit("a", CoordinationErrorKind::Fenced);
1542 d.commit(&mut s, &[(a_partition, 10), (b_partition, 20)])
1543 .unwrap();
1544 assert_eq!(script.commits().len(), 1);
1546 assert_eq!(script.commits()[0].0.as_str(), "b");
1547 assert_eq!(s.closed, vec!["a"]);
1548
1549 let event = poll(&mut d, &mut s);
1551 assert!(
1552 matches!(event, SourceEvent::LanesRevoked { ref lanes, .. } if lanes[..] == [LaneId(0)])
1553 );
1554
1555 s.encoded.clear();
1557 d.commit(&mut s, &[(a_partition, 11)]).unwrap();
1558 assert!(s.encoded.is_empty());
1559
1560 script.push(vec![
1564 CoordinationEvent::Lost {
1565 split: SplitId::new("a").unwrap(),
1566 },
1567 gained("a", 3, Some(10)),
1568 ]);
1569 let event = poll(&mut d, &mut s);
1570 let SourceEvent::LanesAdded(lanes) = event else {
1571 panic!("expected an added lane for the re-gain, got {event:?}");
1572 };
1573 assert_eq!(lanes.len(), 1, "only the fresh tenancy's lane");
1574 assert_eq!(s.closed, vec!["a"], "b's fetcher was never detached");
1575 let a_again = s.opened.last().unwrap();
1576 assert_eq!(a_again.0, "a");
1577 assert_eq!(a_again.4, 3, "fresh tenancy under the new epoch");
1578 assert_ne!(a_again.3, a_partition, "fresh partition — no reuse");
1579 assert_eq!(a_again.2, LaneId(2), "fresh lane id — never reused");
1580 }
1581
1582 #[test]
1583 fn lost_then_regained_in_one_batch_is_a_clean_tenancy_swap() {
1584 let script = Script::default();
1585 let mut d = driver(&script);
1586 let mut s = TestSource::default();
1587 script.push(vec![gained("a", 1, None)]);
1588 poll(&mut d, &mut s);
1589 let first_partition = s.opened[0].3;
1590
1591 script.push(vec![
1592 CoordinationEvent::Lost {
1593 split: SplitId::new("a").unwrap(),
1594 },
1595 gained("a", 2, Some(5)),
1596 ]);
1597 let event = poll(&mut d, &mut s);
1599 assert!(matches!(event, SourceEvent::LanesRevoked { .. }));
1600 let event = poll(&mut d, &mut s);
1601 assert!(matches!(event, SourceEvent::LanesAdded(ref l) if l.len() == 1));
1602 let reopened = s.opened.last().unwrap();
1603 assert_eq!(reopened.4, 2);
1604 assert_eq!(reopened.1, Some(5), "resume from the carried progress");
1605 assert_ne!(reopened.3, first_partition);
1606 assert_eq!(reopened.2, LaneId(1), "lane ids are never recycled");
1607 }
1608
1609 #[test]
1610 fn retryable_commit_defers_and_recommits_idempotently() {
1611 let script = Script::default();
1612 let mut d = driver(&script);
1613 let mut s = TestSource::default();
1614 script.push(vec![gained("a", 1, None)]);
1615 poll(&mut d, &mut s);
1616 let partition = s.opened[0].3;
1617
1618 script.fail_next_commit("a", CoordinationErrorKind::Retryable);
1619 d.commit(&mut s, &[(partition, 10)]).unwrap();
1620 assert!(script.commits().is_empty(), "deferred, not written");
1621
1622 d.commit(&mut s, &[(partition, 12)]).unwrap();
1624 assert_eq!(script.commits().len(), 1);
1625 assert_eq!(script.commits()[0].1.watermark, 12);
1626 }
1627
1628 #[test]
1629 fn completion_sweep_commits_terminal_progress_and_frees_the_lane() {
1630 let script = Script::default();
1631 let mut d = driver(&script);
1632 let mut s = TestSource::default();
1633 script.push(vec![gained("a", 1, None)]);
1634 poll(&mut d, &mut s);
1635
1636 s.sweeps
1637 .borrow_mut()
1638 .insert("a".into(), SplitProgress::completed(9, vec![]));
1639 let event = poll(&mut d, &mut s);
1643 assert!(
1644 matches!(event, SourceEvent::LanesRetired { ref lanes } if lanes[..] == [LaneId(0)]),
1645 "completed lanes retire without a drain barrier, got {event:?}"
1646 );
1647 assert_eq!(script.commits().len(), 1);
1648 assert!(script.commits()[0].1.completed);
1649
1650 script.push(vec![gained("b", 1, None)]);
1652 poll(&mut d, &mut s);
1653 let b_partition = s.opened.last().unwrap().3;
1654 s.complete_at.insert("b".into(), 20);
1655 d.commit(&mut s, &[(b_partition, 20)]).unwrap();
1656 assert!(script.commits().last().unwrap().1.completed);
1657 let event = poll(&mut d, &mut s);
1658 assert!(matches!(event, SourceEvent::LanesRetired { .. }));
1659 }
1660
1661 #[test]
1662 fn standby_with_zero_splits_drains_on_all_complete() {
1663 let script = Script::default();
1664 let mut d = driver(&script);
1665 let mut s = TestSource::default();
1666
1667 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
1668 script.push(vec![CoordinationEvent::AllComplete]);
1669 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Drained));
1670 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Drained));
1672 }
1673
1674 #[test]
1675 fn stalled_is_fatal_by_default_and_drains_when_configured() {
1676 let script = Script::default();
1677 let mut d = driver(&script);
1678 let mut s = TestSource::default();
1679 script.push(vec![CoordinationEvent::Stalled {
1680 completed: 7,
1681 quarantined: 1,
1682 }]);
1683 let err = d.poll_events(&mut s, Duration::ZERO).unwrap_err();
1685 assert!(err.to_string().contains("quarantined"), "{err}");
1686
1687 let script = Script::default();
1688 let mut d = CoordinationDriver::new(Box::new(ScriptedCoordinator(script.clone())))
1689 .stall_drains(true);
1690 let _: SourceEvent<StubLane> = d.start(Box::new(NoopPlanner)).unwrap();
1691 script.push(vec![CoordinationEvent::Stalled {
1692 completed: 7,
1693 quarantined: 1,
1694 }]);
1695 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Drained));
1696 }
1697
1698 #[test]
1699 fn fail_reports_poison_and_retires_the_lane() {
1700 let script = Script::default();
1701 let mut d = driver(&script);
1702 let mut s = TestSource::default();
1703 script.push(vec![gained("a", 1, None)]);
1704 poll(&mut d, &mut s);
1705
1706 d.fail(&mut s, &SplitId::new("a").unwrap(), "undecodable object")
1707 .unwrap();
1708 assert_eq!(script.fails().len(), 1);
1709 assert_eq!(s.closed, vec!["a"]);
1710 let event = poll(&mut d, &mut s);
1711 assert!(matches!(event, SourceEvent::LanesRevoked { .. }));
1712 d.fail(&mut s, &SplitId::new("a").unwrap(), "again")
1714 .unwrap();
1715 assert_eq!(script.fails().len(), 1);
1716 }
1717
1718 #[test]
1719 fn release_hands_back_every_live_split() {
1720 let script = Script::default();
1721 let mut d = driver(&script);
1722 let mut s = TestSource::default();
1723 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
1724 poll(&mut d, &mut s);
1725
1726 d.release();
1727 let released = script.released();
1728 assert_eq!(released.len(), 2);
1729 assert!(released.iter().any(|s| s.as_str() == "a"));
1730 assert!(released.iter().any(|s| s.as_str() == "b"));
1731 }
1732
1733 #[test]
1734 fn resume_validation_rejects_drifted_progress() {
1735 let script = Script::default();
1736 let mut d = driver(&script);
1737 let mut s = TestSource {
1738 reject_resume: true,
1739 ..TestSource::default()
1740 };
1741 script.push(vec![gained("a", 1, Some(7))]);
1742 let err = d.poll_events(&mut s, Duration::ZERO).unwrap_err();
1743 assert!(err.to_string().contains("resume drift"), "{err}");
1744 }
1745
1746 #[test]
1751 fn a_drain_keeps_the_tenancy_commit_eligible_until_the_final_commit() {
1752 let script = Script::default();
1756 let mut d = driver(&script);
1757 let mut s = TestSource {
1758 accept_revoke: HashSet::from(["a".to_string()]),
1759 ..TestSource::default()
1760 };
1761 script.push(vec![gained("a", 1, None)]);
1762 poll(&mut d, &mut s);
1763 let partition = s.opened[0].3;
1764
1765 script.push(vec![CoordinationEvent::RevokeRequested {
1768 split: SplitId::new("a").unwrap(),
1769 }]);
1770 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
1771 assert_eq!(
1772 s.begin_revoke_calls,
1773 ["a"],
1774 "the source was asked to stop intake"
1775 );
1776
1777 d.commit(&mut s, &[(partition, 42)]).unwrap();
1779 assert_eq!(s.encoded, vec![("a".to_string(), 42)]);
1780 assert_eq!(script.commits().len(), 1);
1781 assert_eq!(script.commits()[0].0.as_str(), "a");
1782 assert!(
1783 !script.commits()[0].1.completed,
1784 "a revocation never completes the split"
1785 );
1786 assert!(
1787 script.released_drained().is_empty(),
1788 "not released while the drain is still in flight"
1789 );
1790 }
1791
1792 #[test]
1793 fn a_completed_drain_releases_exactly_one_split_and_retires_barrierless() {
1794 let script = Script::default();
1795 let mut d = driver(&script);
1796 let mut s = TestSource {
1797 accept_revoke: HashSet::from(["a".to_string()]),
1798 ..TestSource::default()
1799 };
1800 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
1801 poll(&mut d, &mut s);
1802
1803 s.ready_progress
1806 .borrow_mut()
1807 .insert("a".into(), SplitProgress::new(50, vec![]));
1808 script.push(vec![CoordinationEvent::RevokeRequested {
1809 split: SplitId::new("a").unwrap(),
1810 }]);
1811
1812 let event = poll(&mut d, &mut s);
1815 let SourceEvent::LanesRetired { lanes } = event else {
1816 panic!("a cooperative revocation must retire barrier-less, got {event:?}");
1817 };
1818 assert_eq!(lanes, vec![LaneId(0)], "only a's lane leaves");
1819
1820 assert_eq!(
1823 script.released_drained(),
1824 vec![SplitId::new("a").unwrap()],
1825 "exactly one split, released via the revocation path"
1826 );
1827 assert!(script.released().is_empty(), "not a plain hand-back");
1828 let last = script.commits().last().cloned().unwrap();
1830 assert_eq!(last.0.as_str(), "a");
1831 assert_eq!(last.1.watermark, 50);
1832 assert!(!last.1.completed, "drain commits never complete the split");
1833
1834 assert_eq!(s.closed, vec!["a"], "b's fetcher stays attached");
1836 assert!(
1837 d.assignments().iter().any(|(id, _)| id.as_str() == "b"),
1838 "b is still owned"
1839 );
1840 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
1841 }
1842
1843 #[test]
1844 fn a_fenced_final_commit_aborts_the_drain_into_a_revoke() {
1845 let script = Script::default();
1846 let mut d = driver(&script);
1847 let mut s = TestSource {
1848 accept_revoke: HashSet::from(["a".to_string()]),
1849 ..TestSource::default()
1850 };
1851 script.push(vec![gained("a", 1, None)]);
1852 poll(&mut d, &mut s);
1853 let partition = s.opened[0].3;
1854
1855 s.ready_progress
1858 .borrow_mut()
1859 .insert("a".into(), SplitProgress::new(50, vec![]));
1860 script.fail_next_commit("a", CoordinationErrorKind::Fenced);
1861 script.push(vec![CoordinationEvent::RevokeRequested {
1862 split: SplitId::new("a").unwrap(),
1863 }]);
1864
1865 let event = poll(&mut d, &mut s);
1869 let SourceEvent::LanesRevoked { lanes, barrier } = event else {
1870 panic!("a fenced drain must abort into a revoke, got {event:?}");
1871 };
1872 assert_eq!(lanes, vec![LaneId(0)]);
1873 assert_eq!(barrier.remaining(), 1, "one party per revoked lane");
1874 assert!(
1875 script.released_drained().is_empty(),
1876 "a fenced drain never releases"
1877 );
1878 assert!(
1879 script.commits().is_empty(),
1880 "the fenced final commit wrote nothing"
1881 );
1882 assert_eq!(s.closed, vec!["a"], "the fetcher was detached on the abort");
1883
1884 d.commit(&mut s, &[(partition, 60)]).unwrap();
1886 assert!(
1887 s.encoded.is_empty(),
1888 "a retired-fenced tenancy must not encode"
1889 );
1890 }
1891
1892 #[test]
1893 fn a_retryable_final_commit_keeps_the_drain_pending() {
1894 let script = Script::default();
1895 let mut d = driver(&script);
1896 let mut s = TestSource {
1897 accept_revoke: HashSet::from(["a".to_string()]),
1898 ..TestSource::default()
1899 };
1900 script.push(vec![gained("a", 1, None)]);
1901 poll(&mut d, &mut s);
1902
1903 s.ready_progress
1905 .borrow_mut()
1906 .insert("a".into(), SplitProgress::new(50, vec![]));
1907 script.fail_next_commit("a", CoordinationErrorKind::Retryable);
1908 script.push(vec![CoordinationEvent::RevokeRequested {
1909 split: SplitId::new("a").unwrap(),
1910 }]);
1911
1912 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
1915 assert!(script.commits().is_empty(), "deferred, not written");
1916 assert!(script.released_drained().is_empty());
1917 assert!(
1918 d.assignments().iter().any(|(id, _)| id.as_str() == "a"),
1919 "still owned while the final commit retries"
1920 );
1921
1922 let event = poll(&mut d, &mut s);
1925 let SourceEvent::LanesRetired { lanes } = event else {
1926 panic!("the retried drain must finally retire, got {event:?}");
1927 };
1928 assert_eq!(lanes, vec![LaneId(0)]);
1929 assert_eq!(script.commits().len(), 1);
1930 assert_eq!(script.commits()[0].1.watermark, 50);
1931 assert_eq!(script.released_drained(), vec![SplitId::new("a").unwrap()]);
1932 }
1933
1934 #[test]
1935 fn a_source_that_cannot_stop_intake_declines_the_revoke() {
1936 let script = Script::default();
1937 let mut d = driver(&script);
1938 let mut s = TestSource::default();
1940 script.push(vec![gained("a", 1, None)]);
1941 poll(&mut d, &mut s);
1942 let partition = s.opened[0].3;
1943
1944 script.push(vec![CoordinationEvent::RevokeRequested {
1945 split: SplitId::new("a").unwrap(),
1946 }]);
1947 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
1948
1949 assert_eq!(s.begin_revoke_calls, ["a"]);
1952 assert!(
1953 s.drain_ready_calls.is_empty(),
1954 "a declined split never drains"
1955 );
1956 assert!(script.released_drained().is_empty());
1957
1958 d.commit(&mut s, &[(partition, 30)]).unwrap();
1960 assert_eq!(s.encoded, vec![("a".to_string(), 30)]);
1961 assert_eq!(script.commits().len(), 1);
1962 }
1963
1964 #[test]
1965 fn a_revoke_request_for_an_unheld_split_is_ignored() {
1966 let script = Script::default();
1967 let mut d = driver(&script);
1968 let mut s = TestSource {
1969 accept_revoke: HashSet::from(["a".to_string(), "ghost".to_string()]),
1972 ..TestSource::default()
1973 };
1974 script.push(vec![gained("a", 1, None)]);
1975 poll(&mut d, &mut s);
1976
1977 script.push(vec![CoordinationEvent::RevokeRequested {
1978 split: SplitId::new("ghost").unwrap(),
1979 }]);
1980 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
1981 assert!(
1982 s.begin_revoke_calls.is_empty(),
1983 "an unheld split must not consult the source"
1984 );
1985 assert!(
1986 d.assignments().iter().any(|(id, _)| id.as_str() == "a"),
1987 "the held split is untouched"
1988 );
1989 }
1990
1991 #[test]
1992 fn a_source_that_declines_feeds_the_decline_back() {
1993 let script = Script::default();
1998 let mut d = driver(&script);
1999 let mut s = TestSource::default();
2001 script.push(vec![gained("a", 1, None)]);
2002 poll(&mut d, &mut s);
2003 let partition = s.opened[0].3;
2004
2005 script.push(vec![CoordinationEvent::RevokeRequested {
2006 split: SplitId::new("a").unwrap(),
2007 }]);
2008 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2009
2010 assert_eq!(s.begin_revoke_calls, ["a"]);
2013 assert_eq!(
2014 script.declined(),
2015 vec![SplitId::new("a").unwrap()],
2016 "the source's refusal must reach the backend, once"
2017 );
2018
2019 d.commit(&mut s, &[(partition, 30)]).unwrap();
2021 assert_eq!(s.encoded, vec![("a".to_string(), 30)]);
2022 assert_eq!(script.commits().len(), 1);
2023 assert_eq!(script.commits()[0].0.as_str(), "a");
2024 }
2025
2026 #[test]
2027 fn a_repeated_revoke_request_mid_drain_is_not_declined() {
2028 let script = Script::default();
2035 let mut d = driver(&script);
2036 let mut s = TestSource {
2037 accept_revoke: HashSet::from(["a".to_string()]),
2038 ..TestSource::default()
2039 };
2040 script.push(vec![gained("a", 1, None)]);
2041 poll(&mut d, &mut s);
2042
2043 script.push(vec![CoordinationEvent::RevokeRequested {
2046 split: SplitId::new("a").unwrap(),
2047 }]);
2048 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2049 assert_eq!(s.begin_revoke_calls, ["a"]);
2050
2051 script.push(vec![CoordinationEvent::RevokeRequested {
2053 split: SplitId::new("a").unwrap(),
2054 }]);
2055 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2056 assert_eq!(
2057 s.begin_revoke_calls,
2058 ["a"],
2059 "the source must not be asked to stop intake it has already stopped"
2060 );
2061 assert!(
2062 script.declined().is_empty(),
2063 "a drain already in flight satisfies the request; declining it would force the release"
2064 );
2065
2066 s.ready_progress
2068 .borrow_mut()
2069 .insert("a".into(), SplitProgress::new(50, vec![]));
2070 let event = poll(&mut d, &mut s);
2071 let SourceEvent::LanesRetired { lanes } = event else {
2072 panic!("the drain must still retire, got {event:?}");
2073 };
2074 assert_eq!(lanes, vec![LaneId(0)]);
2075 assert_eq!(script.released_drained(), vec![SplitId::new("a").unwrap()]);
2076 }
2077
2078 #[test]
2079 fn an_unopened_tenancy_declines_without_asking_the_source() {
2080 let script = Script::default();
2086 let mut d = driver(&script);
2087 let mut s = TestSource {
2090 accept_revoke: HashSet::from(["a".to_string()]),
2091 ..TestSource::default()
2092 };
2093
2094 script.push(vec![
2095 gained("a", 1, None),
2096 CoordinationEvent::RevokeRequested {
2097 split: SplitId::new("a").unwrap(),
2098 },
2099 ]);
2100 let event = poll(&mut d, &mut s);
2103 let SourceEvent::LanesAdded(lanes) = event else {
2104 panic!("the split must still open after the early decline, got {event:?}");
2105 };
2106 assert_eq!(lanes.len(), 1);
2107
2108 assert!(
2109 s.begin_revoke_calls.is_empty(),
2110 "a not-yet-opened tenancy must never be asked to stop intake"
2111 );
2112 assert_eq!(
2113 script.declined(),
2114 vec![SplitId::new("a").unwrap()],
2115 "the premature request is declined back to the backend"
2116 );
2117
2118 let partition = s.opened[0].3;
2120 d.commit(&mut s, &[(partition, 25)]).unwrap();
2121 assert_eq!(s.encoded, vec![("a".to_string(), 25)]);
2122 assert_eq!(script.commits().len(), 1);
2123 assert!(script.released_drained().is_empty());
2125 }
2126
2127 #[test]
2128 fn a_completed_progress_during_a_drain_is_never_terminal() {
2129 let script = Script::default();
2135 let mut d = driver(&script);
2136 let mut s = TestSource {
2137 accept_revoke: HashSet::from(["a".to_string()]),
2138 ..TestSource::default()
2139 };
2140 script.push(vec![gained("a", 1, None)]);
2141 poll(&mut d, &mut s);
2142 let partition = s.opened[0].3;
2143
2144 script.push(vec![CoordinationEvent::RevokeRequested {
2147 split: SplitId::new("a").unwrap(),
2148 }]);
2149 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2150 assert_eq!(s.begin_revoke_calls, ["a"]);
2151
2152 s.complete_at.insert("a".into(), 42);
2155 d.commit(&mut s, &[(partition, 42)]).unwrap();
2156
2157 let committed = script.commits().last().cloned().expect("the commit landed");
2159 assert_eq!(committed.0.as_str(), "a");
2160 assert_eq!(
2161 committed.1.watermark, 42,
2162 "the guard strips the flag, not the commit"
2163 );
2164 assert!(
2165 !committed.1.completed,
2166 "a drain cut is never terminal, whatever the source claims"
2167 );
2168
2169 assert!(
2172 d.assignments().iter().any(|(id, _)| id.as_str() == "a"),
2173 "still owned"
2174 );
2175 assert!(s.closed.is_empty(), "not retired");
2176
2177 s.ready_progress
2179 .borrow_mut()
2180 .insert("a".into(), SplitProgress::new(50, vec![]));
2181 let event = poll(&mut d, &mut s);
2182 let SourceEvent::LanesRetired { lanes } = event else {
2183 panic!("the drained revocation must finally retire, got {event:?}");
2184 };
2185 assert_eq!(lanes, vec![LaneId(0)]);
2186 assert_eq!(script.released_drained(), vec![SplitId::new("a").unwrap()]);
2187 assert!(
2188 !script.commits().last().unwrap().1.completed,
2189 "the final revocation commit is not terminal either"
2190 );
2191 }
2192}