1#![expect(deprecated, reason = "v0.4.0 removes this with spate-coordination")]
49
50use super::{
51 ControlWaker, CoordinationError, CoordinationErrorKind, CoordinationEvent, LeaseEpoch,
52 SplitCoordinator, SplitId, SplitPlanner, SplitProgress, SplitSpec,
53};
54use crate::error::{ErrorClass, SourceError};
55use crate::record::PartitionId;
56use crate::source::{DrainBarrier, LaneId, SourceEvent, SourceLane};
57use std::collections::BTreeMap;
58use std::fmt;
59use std::time::Duration;
60
61#[derive(Debug)]
65#[non_exhaustive]
66pub struct SplitOpening<'a> {
67 pub split: &'a SplitSpec,
69 pub resume: Option<&'a SplitProgress>,
72 pub lane: LaneId,
75 pub partition: PartitionId,
78 pub epoch: LeaseEpoch,
80 pub waker: &'a ControlWaker,
85}
86
87#[deprecated(
94 since = "0.3.0",
95 note = "removed in v0.4.0, where a work feed replaces split coordination; see https://github.com/spate-etl/spate/issues/1088"
96)]
97pub trait SplitSource {
98 type Lane: SourceLane;
100
101 fn open_split(&mut self, opening: SplitOpening<'_>) -> Result<Self::Lane, SourceError>;
104
105 fn validate_resume(
117 &self,
118 split: &SplitSpec,
119 progress: &SplitProgress,
120 ) -> Result<(), SourceError> {
121 let _ = (split, progress);
122 Ok(())
123 }
124
125 fn encode_commit(
130 &mut self,
131 split: &SplitId,
132 watermark: i64,
133 ) -> Result<SplitProgress, SourceError>;
134
135 fn sweep(&mut self, split: &SplitId) -> Result<Option<SplitProgress>, SourceError>;
140
141 fn close_split(&mut self, split: &SplitId);
151
152 fn take_finishing(&mut self) -> Vec<SplitId> {
160 Vec::new()
161 }
162
163 fn begin_revoke(&mut self, split: &SplitId) -> bool {
194 let _ = split;
195 false
196 }
197
198 fn drain_ready(&mut self, split: &SplitId) -> Result<Option<SplitProgress>, SourceError> {
219 let _ = split;
220 Ok(None)
221 }
222}
223
224#[derive(Debug, PartialEq, Eq, Clone, Copy)]
225enum TenancyState {
226 Live,
228 Draining,
236 Retired,
240}
241
242#[derive(Debug)]
243struct Tenancy {
244 split: SplitSpec,
245 epoch: LeaseEpoch,
246 lane: Option<LaneId>,
247 state: TenancyState,
248 fenced: bool,
250 progress: Option<SplitProgress>,
255 completed: bool,
257 handed_off: bool,
262}
263
264#[derive(Debug)]
266struct QueuedReport {
267 split: SplitId,
268 epoch: LeaseEpoch,
270 reason: String,
271}
272
273#[deprecated(
276 since = "0.3.0",
277 note = "removed in v0.4.0, where a work feed replaces split coordination; see https://github.com/spate-etl/spate/issues/1088"
278)]
279pub struct CoordinationDriver {
280 coordinator: Box<dyn SplitCoordinator>,
281 wait: crossbeam_channel::Receiver<()>,
284 waker: ControlWaker,
285 tenancies: BTreeMap<PartitionId, Tenancy>,
286 by_split: BTreeMap<SplitId, PartitionId>,
287 pending_lost: Vec<LaneId>,
289 pending_retired: Vec<LaneId>,
292 pending_open: Vec<PartitionId>,
295 pending_poison: Vec<QueuedReport>,
300 all_complete: bool,
301 stalled: Option<(u64, u64)>,
302 stall_drains: bool,
303 started: bool,
304 next_partition: u32,
305 next_lane: u32,
307 stop: crate::source::StopSignal,
308}
309
310impl fmt::Debug for CoordinationDriver {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 f.debug_struct("CoordinationDriver")
313 .field("tenancies", &self.tenancies.len())
314 .field("live", &self.by_split.len())
315 .field("pending_lost", &self.pending_lost.len())
316 .field("pending_retired", &self.pending_retired.len())
317 .field("pending_open", &self.pending_open.len())
318 .field("all_complete", &self.all_complete)
319 .field("stalled", &self.stalled)
320 .field("started", &self.started)
321 .finish_non_exhaustive()
322 }
323}
324
325impl CoordinationDriver {
326 #[must_use]
328 pub fn new(mut coordinator: Box<dyn SplitCoordinator>) -> CoordinationDriver {
329 let (waker, wait) = super::control_channel();
330 coordinator.set_waker(waker.clone());
331 CoordinationDriver {
332 coordinator,
333 wait,
334 waker,
335 tenancies: BTreeMap::new(),
336 by_split: BTreeMap::new(),
337 pending_lost: Vec::new(),
338 pending_retired: Vec::new(),
339 pending_open: Vec::new(),
340 pending_poison: Vec::new(),
341 all_complete: false,
342 stalled: None,
343 stall_drains: false,
344 started: false,
345 next_partition: 0,
346 next_lane: 0,
347 stop: crate::source::StopSignal::default(),
348 }
349 }
350
351 #[must_use]
356 pub fn stall_drains(mut self, drains: bool) -> CoordinationDriver {
357 self.stall_drains = drains;
358 self
359 }
360
361 pub fn start<L>(
366 &mut self,
367 planner: Box<dyn SplitPlanner>,
368 ) -> Result<SourceEvent<L>, SourceError> {
369 assert!(!self.started, "CoordinationDriver::start called twice");
370 self.coordinator.start(planner).map_err(as_source_error)?;
371 self.started = true;
372 Ok(SourceEvent::LanesAssigned(Vec::new()))
373 }
374
375 pub fn set_stop(&mut self, stop: crate::source::StopSignal) {
378 self.coordinator.set_stop(stop.clone());
379 self.stop = stop;
380 }
381
382 pub fn poll_events<S: SplitSource>(
385 &mut self,
386 source: &mut S,
387 timeout: Duration,
388 ) -> Result<SourceEvent<S::Lane>, SourceError> {
389 assert!(self.started, "poll_events before start");
390
391 let mut park = timeout;
396 loop {
397 if !self.pending_lost.is_empty() {
399 let lanes = std::mem::take(&mut self.pending_lost);
400 let barrier = DrainBarrier::new(lanes.len());
401 return Ok(SourceEvent::LanesRevoked { lanes, barrier });
402 }
403
404 if !self.pending_retired.is_empty() {
406 let lanes = std::mem::take(&mut self.pending_retired);
407 return Ok(SourceEvent::LanesRetired { lanes });
408 }
409
410 self.tenancies
415 .retain(|_, t| t.state != TenancyState::Retired);
416
417 if !self.pending_open.is_empty() {
420 let lanes = self.open_pending(source)?;
421 if !lanes.is_empty() {
422 return Ok(SourceEvent::LanesAdded(lanes));
423 }
424 }
425
426 if let Some((completed, quarantined)) = self.stalled {
428 if self.stall_drains {
429 tracing::warn!(
430 completed,
431 quarantined,
432 "job stalled; draining as configured"
433 );
434 return Ok(SourceEvent::Drained);
435 }
436 return Err(SourceError::Client {
437 class: ErrorClass::Fatal,
438 reason: format!(
439 "coordinated job stalled: {completed} splits completed but {quarantined} \
440 are quarantined and out of delivery attempts; inspect \
441 spate_coordination_splits_quarantined and requeue or exclude them"
442 ),
443 });
444 }
445 if self.all_complete {
446 return Ok(SourceEvent::Drained);
447 }
448
449 let events = self.coordinator.poll().map_err(as_source_error)?;
452
453 for event in &events {
459 if let CoordinationEvent::Lost { split }
460 | CoordinationEvent::Quarantined { split, .. } = event
461 {
462 self.drop_poison_reports(split);
463 }
464 }
465 for report in std::mem::take(&mut self.pending_poison) {
466 if !self.report_poison(&report.split, report.epoch, &report.reason) {
467 self.pending_poison.push(report);
468 } else if let Some(partition) = self.tenancy_at(&report.split, report.epoch) {
469 self.retire(source, partition, false);
470 }
471 }
472
473 let mut surfaced: Option<SourceError> = None;
479 for event in events {
480 if let Err(e) = self.apply(source, event) {
481 match &surfaced {
487 Some(kept) if is_fatal(kept) || !is_fatal(&e) => {}
488 _ => surfaced = Some(e),
489 }
490 }
491 }
492 if let Some(e) = surfaced {
493 return Err(e);
494 }
495
496 self.sweep(source)?;
498
499 self.advance_drains(source)?;
501
502 if !self.pending_lost.is_empty()
503 || !self.pending_retired.is_empty()
504 || !self.pending_open.is_empty()
505 || self.all_complete
506 || self.stalled.is_some()
507 {
508 park = Duration::ZERO;
511 continue;
512 }
513 break;
514 }
515
516 let finishing = source.take_finishing();
519 if !finishing.is_empty() {
520 let partitions: Vec<PartitionId> = finishing
521 .iter()
522 .filter_map(|split| self.by_split.get(split).copied())
523 .collect();
524 if !partitions.is_empty() {
525 return Ok(SourceEvent::CommitReady { partitions });
526 }
527 }
528
529 if !park.is_zero() {
533 let _ = self.wait.recv_timeout(park);
534 }
535 Ok(SourceEvent::Idle)
536 }
537
538 pub fn commit<S: SplitSource>(
545 &mut self,
546 source: &mut S,
547 watermarks: &[(PartitionId, i64)],
548 ) -> Result<(), SourceError> {
549 let mut deferred = 0usize;
550 let mut unsent = 0usize;
551 for (i, &(partition, watermark)) in watermarks.iter().enumerate() {
552 if self.stop.is_set() {
553 unsent = watermarks.len() - i;
554 break;
555 }
556 let Some(split) = self.commit_target(partition) else {
557 continue;
558 };
559 let progress = source.encode_commit(&split, watermark)?;
560 if self.commit_progress(source, partition, &split, progress)?
561 == CommitDisposition::Deferred
562 {
563 deferred += 1;
564 }
565 }
566 if unsent > 0 || (deferred > 0 && self.stop.is_set()) {
567 let not_sent = if unsent > 0 {
568 format!(" and {unsent} not sent")
569 } else {
570 String::new()
571 };
572 return Err(SourceError::Client {
573 class: ErrorClass::Retryable,
574 reason: format!(
575 "{deferred} split commit(s) deferred{not_sent} because the stop began; the \
576 positions stay pending"
577 ),
578 });
579 }
580 if deferred > 0 {
581 return Err(SourceError::Client {
582 class: ErrorClass::Retryable,
583 reason: format!("{deferred} split commit(s) deferred; the positions stay pending"),
584 });
585 }
586 Ok(())
587 }
588
589 pub fn commit_final<S: SplitSource>(
597 &mut self,
598 source: &mut S,
599 watermarks: &[(PartitionId, i64)],
600 ) -> Result<Vec<PartitionId>, SourceError> {
601 let mut partitions = Vec::new();
602 let mut batch = Vec::new();
603 for &(partition, watermark) in watermarks {
604 let Some(split) = self.commit_target(partition) else {
605 continue;
606 };
607 let progress = source.encode_commit(&split, watermark)?;
608 let progress = self.strip_drain_completion(partition, &split, progress);
609 partitions.push(partition);
610 batch.push((split, progress));
611 }
612 if batch.is_empty() {
613 return Ok(Vec::new());
614 }
615 let answers = self.coordinator.commit_final(&batch);
616 debug_assert_eq!(answers.len(), batch.len(), "one answer per commit");
617 let mut answers = answers.into_iter();
618 let mut fatal = None;
619 let mut unstored = Vec::new();
620 let mut deferred = Vec::new();
621 for (partition, (split, progress)) in partitions.into_iter().zip(batch) {
622 let answer = answers.next().unwrap_or_else(|| {
624 Err(CoordinationError::new(
625 CoordinationErrorKind::Retryable,
626 "no answer for this commit",
627 ))
628 });
629 let cause = answer.as_ref().err().map(ToString::to_string);
630 match self.classify(source, partition, &split, answer) {
631 Ok(disposition) => {
632 if matches!(disposition, CommitDisposition::Deferred) {
633 unstored.push(partition);
634 deferred.push((split, cause.unwrap_or_default()));
635 }
636 self.settle_commit(source, partition, progress, disposition);
637 }
638 Err(e) => {
639 fatal.get_or_insert(e);
640 }
641 }
642 }
643 if let Some(e) = fatal {
644 return Err(e);
645 }
646 if !unstored.is_empty() {
647 tracing::warn!(
648 splits = ?deferred,
649 "final commit left {} split(s) uncommitted; their progress since the last \
650 durable commit may replay under the next owner",
651 deferred.len()
652 );
653 }
654 Ok(unstored)
655 }
656
657 fn commit_target(&self, partition: PartitionId) -> Option<SplitId> {
660 let tenancy = self.tenancies.get(&partition)?;
664 if tenancy.state == TenancyState::Retired || tenancy.fenced || tenancy.completed {
665 return None;
666 }
667 Some(tenancy.split.id.clone())
668 }
669
670 pub fn fail<S: SplitSource>(
677 &mut self,
678 source: &mut S,
679 split: &SplitId,
680 epoch: LeaseEpoch,
681 reason: &str,
682 ) -> Result<(), SourceError> {
683 let Some(partition) = self.tenancy_at(split, epoch) else {
684 let held_epoch = self
685 .by_split
686 .get(split)
687 .and_then(|p| self.tenancies.get(p))
688 .map(|t| t.epoch.0);
689 tracing::debug!(
690 split = %split,
691 reported_epoch = epoch.0,
692 held_epoch,
693 "dropping a failure report for a tenancy this instance does not hold"
694 );
695 return Ok(());
696 };
697 match self.coordinator.fail(split, epoch, reason) {
698 Ok(()) => {}
699 Err(e) if e.kind == CoordinationErrorKind::Fenced => {}
701 Err(e) if e.kind == CoordinationErrorKind::Retryable => {
704 tracing::warn!(
705 split = %split,
706 error = %e,
707 "poison report refused; retrying while this instance holds the split"
708 );
709 if !self
710 .pending_poison
711 .iter()
712 .any(|queued| &queued.split == split && queued.epoch == epoch)
713 {
714 self.pending_poison.push(QueuedReport {
715 split: split.clone(),
716 epoch,
717 reason: reason.to_string(),
718 });
719 }
720 return Ok(());
721 }
722 Err(e) => return Err(as_source_error(e)),
723 }
724 self.retire(source, partition, false);
725 Ok(())
726 }
727
728 pub fn release(&mut self) {
732 if !self.started {
733 return;
734 }
735 let held: Vec<SplitId> = self.by_split.keys().cloned().collect();
736 if let Err(e) = self.coordinator.depart(&held) {
737 tracing::warn!(error = %e, "graceful departure reported an error; anything not handed back expires");
738 }
739 }
740
741 #[must_use]
743 pub fn assignments(&self) -> Vec<(SplitId, LaneId)> {
744 self.by_split
745 .iter()
746 .filter_map(|(split, partition)| {
747 let lane = self.tenancies.get(partition)?.lane?;
748 Some((split.clone(), lane))
749 })
750 .collect()
751 }
752
753 fn report_rejected_gain(
758 &mut self,
759 split: &SplitId,
760 epoch: LeaseEpoch,
761 rejection: &SourceError,
762 ) {
763 let reason = format!("carried progress rejected on resume: {rejection}");
764 if !self.report_poison(split, epoch, &reason) {
765 self.pending_poison.push(QueuedReport {
766 split: split.clone(),
767 epoch,
768 reason,
769 });
770 }
771 }
772
773 fn drop_poison_reports(&mut self, split: &SplitId) {
775 self.pending_poison.retain(|queued| &queued.split != split);
776 }
777
778 fn tenancy_at(&self, split: &SplitId, epoch: LeaseEpoch) -> Option<PartitionId> {
780 let &partition = self.by_split.get(split)?;
781 (self.tenancies.get(&partition)?.epoch == epoch).then_some(partition)
782 }
783
784 fn report_poison(&mut self, split: &SplitId, epoch: LeaseEpoch, reason: &str) -> bool {
787 match self.coordinator.fail(split, epoch, reason) {
788 Ok(()) => true,
789 Err(e) if e.kind == CoordinationErrorKind::Fenced => true,
791 Err(e) => {
792 tracing::warn!(
793 split = %split,
794 error = %e,
795 "poison report refused; retrying while this instance holds the split"
796 );
797 false
798 }
799 }
800 }
801
802 fn apply<S: SplitSource>(
803 &mut self,
804 source: &mut S,
805 event: CoordinationEvent,
806 ) -> Result<(), SourceError> {
807 match event {
808 CoordinationEvent::Gained {
809 split,
810 epoch,
811 progress,
812 } => {
813 if let Some(&stale) = self.by_split.get(&split.id) {
814 tracing::warn!(split = %split.id, "gained a split already held; retiring stale tenancy");
817 self.retire(source, stale, false);
818 }
819 if let Some(progress) = progress.as_ref()
820 && let Err(e) = source.validate_resume(&split, progress)
821 {
822 self.report_rejected_gain(&split.id, epoch, &e);
826 return Err(e);
827 }
828 let partition = PartitionId(self.next_partition);
829 self.next_partition += 1;
830 self.by_split.insert(split.id.clone(), partition);
831 self.tenancies.insert(
832 partition,
833 Tenancy {
834 split,
835 epoch,
836 lane: None,
837 state: TenancyState::Live,
838 fenced: false,
839 progress,
840 completed: false,
841 handed_off: false,
842 },
843 );
844 self.pending_open.push(partition);
845 }
846 CoordinationEvent::RevokeRequested { split } => {
847 if let Some(&partition) = self.by_split.get(&split)
858 && self
859 .tenancies
860 .get(&partition)
861 .is_some_and(|t| t.state == TenancyState::Draining && !t.fenced)
862 {
863 return Ok(());
864 }
865 let accepted = match self.by_split.get(&split) {
866 Some(&partition) => {
867 let eligible = self.tenancies.get(&partition).is_some_and(|t| {
868 t.state == TenancyState::Live
869 && t.lane.is_some()
870 && !t.fenced
871 && !t.completed
872 });
873 if eligible && source.begin_revoke(&split) {
874 if let Some(t) = self.tenancies.get_mut(&partition) {
877 t.state = TenancyState::Draining;
878 }
879 true
880 } else {
881 false
882 }
883 }
884 None => false,
885 };
886 if !accepted && let Err(e) = self.coordinator.decline_revoke(&split) {
887 tracing::warn!(split = %split, error = %e, "revocation decline failed");
890 }
891 }
892 CoordinationEvent::Lost { split } => {
893 self.drop_poison_reports(&split);
894 if let Some(&partition) = self.by_split.get(&split) {
895 self.retire(source, partition, false);
896 }
897 }
899 CoordinationEvent::Quarantined { split, attempts } => {
900 tracing::warn!(split = %split, attempts, "split quarantined");
901 self.drop_poison_reports(&split);
902 if let Some(&partition) = self.by_split.get(&split) {
903 self.retire(source, partition, false);
904 }
905 }
906 CoordinationEvent::AllComplete => {
907 if self.next_partition == 0 {
908 tracing::info!(
909 "coordinated job completed without this instance holding any split — \
910 the job finished before this instance's first rebalance window, or \
911 the fleet has more replicas than splits (see the scaling-out guide)"
912 );
913 }
914 self.all_complete = true;
915 }
916 CoordinationEvent::Stalled {
917 completed,
918 quarantined,
919 } => {
920 self.stalled = Some((completed, quarantined));
921 }
922 }
923 Ok(())
924 }
925
926 fn retire<S: SplitSource>(&mut self, source: &mut S, partition: PartitionId, fenced: bool) {
929 let Some(tenancy) = self.tenancies.get_mut(&partition) else {
930 return;
931 };
932 if tenancy.state == TenancyState::Retired {
933 if fenced {
934 tenancy.fenced = true;
935 }
936 return;
937 }
938 tenancy.state = TenancyState::Retired;
939 tenancy.fenced |= fenced;
940 self.by_split.remove(&tenancy.split.id);
941 let split = tenancy.split.id.clone();
942 self.pending_poison.retain(|queued| queued.split != split);
943 if let Some(lane) = tenancy.lane.take() {
944 if tenancy.completed || tenancy.handed_off {
945 self.pending_retired.push(lane);
946 } else {
947 self.pending_lost.push(lane);
948 }
949 }
950 source.close_split(&split);
951 }
952
953 fn open_pending<S: SplitSource>(
965 &mut self,
966 source: &mut S,
967 ) -> Result<Vec<S::Lane>, SourceError> {
968 let staged = std::mem::take(&mut self.pending_open);
969 let mut lanes = Vec::with_capacity(staged.len());
970 let mut opened: Vec<PartitionId> = Vec::new();
972 for idx in 0..staged.len() {
973 let partition = staged[idx];
974 let Some(tenancy) = self.tenancies.get_mut(&partition) else {
975 continue; };
977 if tenancy.state != TenancyState::Live || tenancy.lane.is_some() {
978 continue;
979 }
980 let lane_id = LaneId(self.next_lane);
981 self.next_lane = self
982 .next_lane
983 .checked_add(1)
984 .expect("lane ids exhausted (u32)");
985 tenancy.lane = Some(lane_id);
986 let opening = SplitOpening {
987 split: &tenancy.split,
988 resume: tenancy.progress.as_ref(),
989 lane: lane_id,
990 partition,
991 epoch: tenancy.epoch,
992 waker: &self.waker,
993 };
994 match source.open_split(opening) {
995 Ok(lane) => {
996 lanes.push(lane);
997 opened.push(partition);
998 }
999 Err(e) => {
1000 drop(lanes);
1004 for p in opened.iter().chain(std::iter::once(&partition)) {
1005 if let Some(t) = self.tenancies.get_mut(p) {
1006 t.lane = None;
1007 }
1008 }
1009 self.pending_open = staged;
1010 return Err(e);
1011 }
1012 }
1013 }
1014 Ok(lanes)
1015 }
1016
1017 fn sweep<S: SplitSource>(&mut self, source: &mut S) -> Result<(), SourceError> {
1018 let candidates: Vec<PartitionId> = self
1019 .tenancies
1020 .iter()
1021 .filter(|(_, t)| t.state == TenancyState::Live && !t.fenced && !t.completed)
1022 .map(|(&p, _)| p)
1023 .collect();
1024 for partition in candidates {
1025 let split = self.tenancies[&partition].split.id.clone();
1026 if let Some(progress) = source.sweep(&split)? {
1027 let _ = self.commit_progress(source, partition, &split, progress)?;
1028 }
1029 }
1030 Ok(())
1031 }
1032
1033 fn advance_drains<S: SplitSource>(&mut self, source: &mut S) -> Result<(), SourceError> {
1046 let draining: Vec<PartitionId> = self
1047 .tenancies
1048 .iter()
1049 .filter(|(_, t)| t.state == TenancyState::Draining && !t.fenced)
1050 .map(|(&p, _)| p)
1051 .collect();
1052 for partition in draining {
1053 let split = self.tenancies[&partition].split.id.clone();
1054 let Some(progress) = source.drain_ready(&split)? else {
1055 continue; };
1057 debug_assert!(
1058 !progress.completed,
1059 "a revocation commit gives the split away, it must not complete it"
1060 );
1061 self.commit_drained(source, partition, &split, progress)?;
1062 }
1063 Ok(())
1064 }
1065
1066 fn try_commit<S: SplitSource>(
1071 &mut self,
1072 source: &mut S,
1073 partition: PartitionId,
1074 split: &SplitId,
1075 progress: &SplitProgress,
1076 ) -> Result<CommitDisposition, SourceError> {
1077 let answer = self.coordinator.commit(split, progress);
1078 if let Err(e) = &answer
1079 && e.kind == CoordinationErrorKind::Retryable
1080 {
1081 tracing::warn!(split = %split, error = %e, "commit deferred; will retry");
1082 }
1083 self.classify(source, partition, split, answer)
1084 }
1085
1086 fn classify<S: SplitSource>(
1089 &mut self,
1090 source: &mut S,
1091 partition: PartitionId,
1092 split: &SplitId,
1093 answer: Result<(), CoordinationError>,
1094 ) -> Result<CommitDisposition, SourceError> {
1095 match answer {
1096 Ok(()) => Ok(CommitDisposition::Durable),
1097 Err(e) if e.kind == CoordinationErrorKind::Fenced => {
1098 tracing::warn!(split = %split, "commit fenced; split lost to a peer");
1103 self.retire(source, partition, true);
1104 Ok(CommitDisposition::Fenced)
1105 }
1106 Err(e) if e.kind == CoordinationErrorKind::Retryable => Ok(CommitDisposition::Deferred),
1107 Err(e) => Err(as_source_error(e)),
1108 }
1109 }
1110
1111 fn strip_drain_completion(
1113 &self,
1114 partition: PartitionId,
1115 split: &SplitId,
1116 progress: SplitProgress,
1117 ) -> SplitProgress {
1118 if progress.completed
1123 && self
1124 .tenancies
1125 .get(&partition)
1126 .is_some_and(|t| t.state == TenancyState::Draining)
1127 {
1128 tracing::error!(
1129 split = %split,
1130 "source reported a draining split completed; forcing \
1131 completed=false — a drain cut is never terminal"
1132 );
1133 SplitProgress::new(progress.watermark, progress.state)
1134 } else {
1135 progress
1136 }
1137 }
1138
1139 fn commit_progress<S: SplitSource>(
1141 &mut self,
1142 source: &mut S,
1143 partition: PartitionId,
1144 split: &SplitId,
1145 progress: SplitProgress,
1146 ) -> Result<CommitDisposition, SourceError> {
1147 let progress = self.strip_drain_completion(partition, split, progress);
1148 let disposition = self.try_commit(source, partition, split, &progress)?;
1149 self.settle_commit(source, partition, progress, disposition);
1150 Ok(disposition)
1151 }
1152
1153 fn settle_commit<S: SplitSource>(
1155 &mut self,
1156 source: &mut S,
1157 partition: PartitionId,
1158 progress: SplitProgress,
1159 disposition: CommitDisposition,
1160 ) {
1161 match disposition {
1162 CommitDisposition::Durable => {
1163 let tenancy = self.tenancies.get_mut(&partition).expect("live tenancy");
1164 let completed = progress.completed;
1165 tenancy.progress = Some(progress);
1166 if completed {
1167 tenancy.completed = true;
1168 self.retire(source, partition, false);
1170 }
1171 }
1172 CommitDisposition::Fenced => {}
1174 CommitDisposition::Deferred => {
1175 let tenancy = self.tenancies.get_mut(&partition).expect("live tenancy");
1182 tenancy.progress = Some(progress);
1183 }
1184 }
1185 }
1186
1187 fn commit_drained<S: SplitSource>(
1192 &mut self,
1193 source: &mut S,
1194 partition: PartitionId,
1195 split: &SplitId,
1196 progress: SplitProgress,
1197 ) -> Result<(), SourceError> {
1198 let progress = if progress.completed {
1200 tracing::error!(
1201 split = %split,
1202 "drain_ready returned completed=true; forcing completed=false — \
1203 a drain cut is never terminal"
1204 );
1205 SplitProgress::new(progress.watermark, progress.state)
1206 } else {
1207 progress
1208 };
1209 match self.try_commit(source, partition, split, &progress)? {
1210 CommitDisposition::Durable => {
1211 let tenancy = self
1212 .tenancies
1213 .get_mut(&partition)
1214 .expect("draining tenancy");
1215 tenancy.progress = Some(progress);
1216 tenancy.handed_off = true;
1217 if let Err(e) = self
1218 .coordinator
1219 .release_drained(std::slice::from_ref(split))
1220 {
1221 tracing::warn!(
1225 split = %split,
1226 error = %e,
1227 "drain release failed; lease will expire and a peer will take over"
1228 );
1229 }
1230 self.retire(source, partition, false);
1231 }
1232 CommitDisposition::Fenced => {}
1234 CommitDisposition::Deferred => {
1235 let tenancy = self
1237 .tenancies
1238 .get_mut(&partition)
1239 .expect("draining tenancy");
1240 tenancy.progress = Some(progress);
1241 }
1242 }
1243 Ok(())
1244 }
1245}
1246
1247#[derive(Clone, Copy, PartialEq, Eq)]
1250enum CommitDisposition {
1251 Durable,
1253 Fenced,
1256 Deferred,
1259}
1260
1261fn is_fatal(e: &SourceError) -> bool {
1262 let SourceError::Client { class, .. } = e;
1263 *class == ErrorClass::Fatal
1264}
1265
1266fn as_source_error(e: CoordinationError) -> SourceError {
1267 SourceError::Client {
1268 class: e.class(),
1269 reason: e.to_string(),
1270 }
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275 use super::*;
1276 use crate::checkpoint::AckRef;
1277 use crate::coordination::{PlanContext, PlanFinality, SplitPlan};
1278 use crate::pipeline::fakes::wait_for;
1279 use crate::record::RawPayload;
1280 use crate::source::PayloadBatch;
1281 use std::cell::RefCell;
1282 use std::collections::{HashMap, HashSet, VecDeque};
1283 use std::rc::Rc;
1284 use std::sync::{Arc, Mutex};
1285 use std::time::Instant;
1286
1287 #[derive(Default)]
1291 struct ScriptState {
1292 batches: VecDeque<Vec<CoordinationEvent>>,
1293 commit_outcomes: HashMap<String, VecDeque<CoordinationErrorKind>>,
1294 commits: Vec<(SplitId, SplitProgress)>,
1295 fail_outcomes: HashMap<String, VecDeque<CoordinationErrorKind>>,
1296 fails: Vec<(SplitId, LeaseEpoch, String)>,
1299 released: Vec<SplitId>,
1300 departed: Option<Vec<SplitId>>,
1302 released_drained: Vec<SplitId>,
1305 declined: Vec<SplitId>,
1308 started: bool,
1309 waker: Option<ControlWaker>,
1310 polls: usize,
1312 stop: Option<crate::source::StopSignal>,
1314 stop_on_commit: Option<(String, Arc<std::sync::atomic::AtomicBool>)>,
1316 }
1317
1318 #[derive(Clone, Default)]
1319 struct Script(Arc<Mutex<ScriptState>>);
1320
1321 impl Script {
1322 fn push(&self, events: Vec<CoordinationEvent>) {
1323 let mut st = self.0.lock().unwrap();
1324 st.batches.push_back(events);
1325 if let Some(w) = &st.waker {
1326 w.wake();
1327 }
1328 }
1329
1330 fn fail_next_commit(&self, split: &str, kind: CoordinationErrorKind) {
1331 self.0
1332 .lock()
1333 .unwrap()
1334 .commit_outcomes
1335 .entry(split.to_string())
1336 .or_default()
1337 .push_back(kind);
1338 }
1339
1340 fn fail_next_report(&self, split: &str, kind: CoordinationErrorKind) {
1341 self.0
1342 .lock()
1343 .unwrap()
1344 .fail_outcomes
1345 .entry(split.to_string())
1346 .or_default()
1347 .push_back(kind);
1348 }
1349
1350 fn commits(&self) -> Vec<(SplitId, SplitProgress)> {
1351 self.0.lock().unwrap().commits.clone()
1352 }
1353
1354 fn released(&self) -> Vec<SplitId> {
1355 self.0.lock().unwrap().released.clone()
1356 }
1357
1358 fn departed(&self) -> Option<Vec<SplitId>> {
1359 self.0.lock().unwrap().departed.clone()
1360 }
1361
1362 fn released_drained(&self) -> Vec<SplitId> {
1363 self.0.lock().unwrap().released_drained.clone()
1364 }
1365
1366 fn declined(&self) -> Vec<SplitId> {
1367 self.0.lock().unwrap().declined.clone()
1368 }
1369
1370 fn fails(&self) -> Vec<(SplitId, LeaseEpoch, String)> {
1371 self.0.lock().unwrap().fails.clone()
1372 }
1373
1374 fn polls(&self) -> usize {
1375 self.0.lock().unwrap().polls
1376 }
1377
1378 fn stop(&self) -> Option<crate::source::StopSignal> {
1379 self.0.lock().unwrap().stop.clone()
1380 }
1381
1382 fn stop_on_commit(&self, split: &str, flag: &Arc<std::sync::atomic::AtomicBool>) {
1383 self.0.lock().unwrap().stop_on_commit = Some((split.to_string(), Arc::clone(flag)));
1384 }
1385 }
1386
1387 struct ScriptedCoordinator(Script);
1388
1389 impl SplitCoordinator for ScriptedCoordinator {
1390 fn start(&mut self, _planner: Box<dyn SplitPlanner>) -> Result<(), CoordinationError> {
1391 self.0.0.lock().unwrap().started = true;
1392 Ok(())
1393 }
1394
1395 fn set_waker(&mut self, waker: ControlWaker) {
1396 self.0.0.lock().unwrap().waker = Some(waker);
1397 }
1398
1399 fn set_stop(&mut self, stop: crate::source::StopSignal) {
1400 self.0.0.lock().unwrap().stop = Some(stop);
1401 }
1402
1403 fn poll(&mut self) -> Result<Vec<CoordinationEvent>, CoordinationError> {
1404 let mut s = self.0.0.lock().unwrap();
1405 s.polls += 1;
1406 Ok(s.batches.pop_front().unwrap_or_default())
1407 }
1408
1409 fn commit(
1410 &mut self,
1411 split: &SplitId,
1412 progress: &SplitProgress,
1413 ) -> Result<(), CoordinationError> {
1414 let mut s = self.0.0.lock().unwrap();
1415 if let Some((target, flag)) = &s.stop_on_commit
1416 && target == split.as_str()
1417 {
1418 flag.store(true, std::sync::atomic::Ordering::SeqCst);
1419 }
1420 if let Some(kinds) = s.commit_outcomes.get_mut(split.as_str())
1421 && let Some(kind) = kinds.pop_front()
1422 {
1423 return Err(CoordinationError::new(kind, "scripted"));
1424 }
1425 s.commits.push((split.clone(), progress.clone()));
1426 Ok(())
1427 }
1428
1429 fn fail(
1430 &mut self,
1431 split: &SplitId,
1432 epoch: LeaseEpoch,
1433 reason: &str,
1434 ) -> Result<(), CoordinationError> {
1435 let mut s = self.0.0.lock().unwrap();
1436 s.fails.push((split.clone(), epoch, reason.to_string()));
1437 if let Some(kinds) = s.fail_outcomes.get_mut(split.as_str())
1438 && let Some(kind) = kinds.pop_front()
1439 {
1440 return Err(CoordinationError::new(kind, "scripted"));
1441 }
1442 Ok(())
1443 }
1444
1445 fn release(&mut self, splits: &[SplitId]) -> Result<(), CoordinationError> {
1446 self.0
1447 .0
1448 .lock()
1449 .unwrap()
1450 .released
1451 .extend(splits.iter().cloned());
1452 Ok(())
1453 }
1454
1455 fn depart(&mut self, held: &[SplitId]) -> Result<(), CoordinationError> {
1456 self.0.0.lock().unwrap().departed = Some(held.to_vec());
1457 Ok(())
1458 }
1459
1460 fn release_drained(&mut self, splits: &[SplitId]) -> Result<(), CoordinationError> {
1461 self.0
1462 .0
1463 .lock()
1464 .unwrap()
1465 .released_drained
1466 .extend(splits.iter().cloned());
1467 Ok(())
1468 }
1469
1470 fn decline_revoke(&mut self, split: &SplitId) -> Result<(), CoordinationError> {
1471 self.0.0.lock().unwrap().declined.push(split.clone());
1472 Ok(())
1473 }
1474 }
1475
1476 struct NoopPlanner;
1477
1478 impl SplitPlanner for NoopPlanner {
1479 fn fingerprint(&self) -> String {
1480 "test:v1".into()
1481 }
1482
1483 fn plan(&mut self, _ctx: PlanContext<'_>) -> Result<SplitPlan, CoordinationError> {
1484 Ok(SplitPlan::new(vec![], PlanFinality::Final))
1485 }
1486 }
1487
1488 enum NoBatch {}
1492
1493 impl<'buf> PayloadBatch<'buf> for NoBatch {
1494 fn next_payload(&mut self) -> Option<RawPayload<'buf>> {
1495 match *self {}
1496 }
1497
1498 fn ack(&self) -> &AckRef {
1499 match *self {}
1500 }
1501 }
1502
1503 #[derive(Debug)]
1504 struct StubLane {
1505 lane: LaneId,
1506 partition: PartitionId,
1507 }
1508
1509 impl SourceLane for StubLane {
1510 type Batch<'a> = NoBatch;
1511
1512 fn id(&self) -> LaneId {
1513 self.lane
1514 }
1515
1516 fn partition(&self) -> PartitionId {
1517 self.partition
1518 }
1519
1520 fn poll(
1521 &mut self,
1522 _max: usize,
1523 _timeout: Duration,
1524 ) -> Result<Option<NoBatch>, SourceError> {
1525 Ok(None)
1526 }
1527 }
1528
1529 #[derive(Default)]
1532 struct TestSource {
1533 opened: Vec<(String, Option<i64>, LaneId, PartitionId, u64)>,
1534 closed: Vec<String>,
1535 encoded: Vec<(String, i64)>,
1536 sweeps: Rc<RefCell<HashMap<String, SplitProgress>>>,
1537 complete_at: HashMap<String, i64>,
1538 reject_resume: HashMap<String, ErrorClass>,
1542 finishing: Vec<String>,
1543 fail_open: Vec<String>,
1546 accept_revoke: HashSet<String>,
1549 begin_revoke_calls: Vec<String>,
1551 drain_ready_calls: Vec<String>,
1554 ready_progress: Rc<RefCell<HashMap<String, SplitProgress>>>,
1558 }
1559
1560 impl SplitSource for TestSource {
1561 type Lane = StubLane;
1562
1563 fn open_split(&mut self, o: SplitOpening<'_>) -> Result<StubLane, SourceError> {
1564 let id = o.split.id.as_str().to_string();
1565 if let Some(i) = self.fail_open.iter().position(|s| *s == id) {
1566 self.fail_open.remove(i);
1567 return Err(SourceError::Client {
1568 class: ErrorClass::Retryable,
1569 reason: format!("open_split failed for {id}"),
1570 });
1571 }
1572 self.opened.push((
1573 o.split.id.as_str().to_string(),
1574 o.resume.map(|p| p.watermark),
1575 o.lane,
1576 o.partition,
1577 o.epoch.0,
1578 ));
1579 Ok(StubLane {
1580 lane: o.lane,
1581 partition: o.partition,
1582 })
1583 }
1584
1585 fn validate_resume(
1586 &self,
1587 split: &SplitSpec,
1588 _progress: &SplitProgress,
1589 ) -> Result<(), SourceError> {
1590 if let Some(&class) = self.reject_resume.get(split.id.as_str()) {
1591 return Err(SourceError::Client {
1592 class,
1593 reason: format!("resume drift on {}", split.id),
1594 });
1595 }
1596 Ok(())
1597 }
1598
1599 fn encode_commit(
1600 &mut self,
1601 split: &SplitId,
1602 watermark: i64,
1603 ) -> Result<SplitProgress, SourceError> {
1604 self.encoded.push((split.as_str().to_string(), watermark));
1605 let completed = self.complete_at.get(split.as_str()) == Some(&watermark);
1606 Ok(if completed {
1607 SplitProgress::completed(watermark, vec![])
1608 } else {
1609 SplitProgress::new(watermark, vec![])
1610 })
1611 }
1612
1613 fn sweep(&mut self, split: &SplitId) -> Result<Option<SplitProgress>, SourceError> {
1614 Ok(self.sweeps.borrow_mut().remove(split.as_str()))
1615 }
1616
1617 fn close_split(&mut self, split: &SplitId) {
1618 self.closed.push(split.as_str().to_string());
1619 }
1620
1621 fn take_finishing(&mut self) -> Vec<SplitId> {
1622 std::mem::take(&mut self.finishing)
1623 .into_iter()
1624 .map(|s| SplitId::new(&s).unwrap())
1625 .collect()
1626 }
1627
1628 fn begin_revoke(&mut self, split: &SplitId) -> bool {
1629 self.begin_revoke_calls.push(split.as_str().to_string());
1630 self.accept_revoke.contains(split.as_str())
1631 }
1632
1633 fn drain_ready(&mut self, split: &SplitId) -> Result<Option<SplitProgress>, SourceError> {
1634 self.drain_ready_calls.push(split.as_str().to_string());
1635 Ok(self.ready_progress.borrow().get(split.as_str()).cloned())
1636 }
1637 }
1638
1639 fn split(id: &str) -> SplitSpec {
1643 SplitSpec::new(SplitId::new(id).unwrap(), format!("desc:{id}").into_bytes())
1644 }
1645
1646 fn gained(id: &str, epoch: u64, watermark: Option<i64>) -> CoordinationEvent {
1647 CoordinationEvent::Gained {
1648 split: split(id),
1649 epoch: LeaseEpoch(epoch),
1650 progress: watermark.map(|w| SplitProgress::new(w, vec![])),
1651 }
1652 }
1653
1654 fn driver(script: &Script) -> CoordinationDriver {
1655 let mut d = CoordinationDriver::new(Box::new(ScriptedCoordinator(script.clone())));
1656 let ready: SourceEvent<StubLane> = d.start(Box::new(NoopPlanner)).unwrap();
1657 assert!(
1658 matches!(ready, SourceEvent::LanesAssigned(ref lanes) if lanes.is_empty()),
1659 "start must return the empty ready signal"
1660 );
1661 d
1662 }
1663
1664 fn poll(d: &mut CoordinationDriver, s: &mut TestSource) -> SourceEvent<StubLane> {
1665 d.poll_events(s, Duration::ZERO).unwrap()
1666 }
1667
1668 fn rejecting(splits: &[&str]) -> TestSource {
1671 TestSource {
1672 reject_resume: splits
1673 .iter()
1674 .map(|s| ((*s).to_string(), ErrorClass::Fatal))
1675 .collect(),
1676 ..TestSource::default()
1677 }
1678 }
1679
1680 #[test]
1684 fn a_signal_cuts_the_control_plane_park_short() {
1685 let script = Script::default();
1691 let mut d = driver(&script);
1692 let mut s = TestSource::default();
1693 let park = Duration::from_millis(400);
1694
1695 let t0 = Instant::now();
1699 assert!(matches!(
1700 d.poll_events(&mut s, park).unwrap(),
1701 SourceEvent::Idle
1702 ));
1703 let idle = t0.elapsed();
1704 assert!(
1705 idle >= park / 2,
1706 "expected a real park, returned after {idle:?}"
1707 );
1708
1709 let signaller = script.clone();
1713 let drained = script.polls();
1714 let handle = std::thread::spawn(move || {
1715 wait_for(
1716 "the driver drains the backend",
1717 Duration::from_secs(5),
1718 || signaller.polls() > drained,
1719 );
1720 signaller.push(vec![CoordinationEvent::AllComplete]);
1721 });
1722 let long_park = Duration::from_secs(30);
1723 let t1 = Instant::now();
1724 assert!(matches!(
1725 d.poll_events(&mut s, long_park).unwrap(),
1726 SourceEvent::Idle
1727 ));
1728 let woken = t1.elapsed();
1729 handle.join().unwrap();
1730 assert!(
1731 woken < long_park / 2,
1732 "a signal must cut the park short, but it ran {woken:?} of {long_park:?}"
1733 );
1734 assert!(matches!(
1735 d.poll_events(&mut s, Duration::ZERO).unwrap(),
1736 SourceEvent::Drained
1737 ));
1738 }
1739
1740 #[test]
1741 fn a_failed_open_undoes_the_whole_batch_instead_of_stranding_lanes() {
1742 let script = Script::default();
1746 let mut d = driver(&script);
1747 let mut s = TestSource {
1748 fail_open: vec!["b".into()],
1749 ..TestSource::default()
1750 };
1751
1752 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
1753 let err = d
1754 .poll_events(&mut s, Duration::ZERO)
1755 .expect_err("the failing open must surface");
1756 assert!(err.to_string().contains("open_split failed for b"), "{err}");
1757 assert_eq!(s.opened.len(), 1, "a opened before b failed");
1758
1759 let event = poll(&mut d, &mut s);
1761 let SourceEvent::LanesAdded(lanes) = event else {
1762 panic!("expected both lanes after the retry, got {event:?}");
1763 };
1764 assert_eq!(lanes.len(), 2);
1765 let reopened: Vec<&str> = s.opened.iter().map(|o| o.0.as_str()).collect();
1766 assert_eq!(reopened, ["a", "a", "b"], "a re-opens on the retry");
1767 assert_eq!(lanes[0].id(), LaneId(2));
1769 assert_eq!(lanes[1].id(), LaneId(3));
1770 }
1771
1772 #[test]
1773 fn gains_coalesce_into_one_added_batch() {
1774 let script = Script::default();
1775 let mut d = driver(&script);
1776 let mut s = TestSource::default();
1777
1778 script.push(vec![gained("b", 1, Some(7)), gained("a", 1, None)]);
1779 let event = poll(&mut d, &mut s);
1780 let SourceEvent::LanesAdded(lanes) = event else {
1781 panic!("expected added lanes, got {event:?}");
1782 };
1783 assert_eq!(lanes.len(), 2);
1784 assert_eq!(s.opened[0].0, "b");
1786 assert_eq!(s.opened[0].2, LaneId(0));
1787 assert_eq!(s.opened[0].1, Some(7), "carried progress reaches open");
1788 assert_eq!(s.opened[1].0, "a");
1789 assert_eq!(s.opened[1].2, LaneId(1));
1790 assert_ne!(s.opened[0].3, s.opened[1].3);
1791 assert_eq!(d.assignments().len(), 2);
1792 }
1793
1794 #[test]
1795 fn a_mid_flow_gain_never_touches_live_lanes_and_their_commits_fold() {
1796 let script = Script::default();
1797 let mut d = driver(&script);
1798 let mut s = TestSource::default();
1799 script.push(vec![gained("a", 1, None)]);
1800 poll(&mut d, &mut s);
1801 let a_partition = s.opened[0].3;
1802
1803 script.push(vec![gained("b", 1, None)]);
1805 let event = poll(&mut d, &mut s);
1806 let SourceEvent::LanesAdded(lanes) = event else {
1807 panic!("expected added lanes, got {event:?}");
1808 };
1809 assert_eq!(lanes.len(), 1, "only the new split's lane");
1810 assert!(
1811 s.closed.is_empty(),
1812 "a routine gain must never detach flowing fetchers"
1813 );
1814
1815 d.commit(&mut s, &[(a_partition, 42)]).unwrap();
1818 assert_eq!(s.encoded, vec![("a".to_string(), 42)]);
1819 assert_eq!(script.commits().len(), 1);
1820 assert_eq!(script.commits()[0].0.as_str(), "a");
1821 assert!(
1823 d.assignments()
1824 .contains(&(SplitId::new("a").unwrap(), LaneId(0)))
1825 );
1826 }
1827
1828 #[test]
1829 fn finishing_splits_surface_as_commit_ready_once() {
1830 let script = Script::default();
1831 let mut d = driver(&script);
1832 let mut s = TestSource::default();
1833 script.push(vec![gained("a", 1, None)]);
1834 poll(&mut d, &mut s);
1835 let a_partition = s.opened[0].3;
1836
1837 s.finishing.push("a".to_string());
1838 let event = poll(&mut d, &mut s);
1839 let SourceEvent::CommitReady { partitions } = event else {
1840 panic!("expected commit-ready, got {event:?}");
1841 };
1842 assert_eq!(partitions, vec![a_partition]);
1843 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
1845 }
1846
1847 #[test]
1848 fn loss_surfaces_as_partial_revoke_and_detaches_fetchers() {
1849 let script = Script::default();
1850 let mut d = driver(&script);
1851 let mut s = TestSource::default();
1852 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
1853 poll(&mut d, &mut s);
1854
1855 script.push(vec![CoordinationEvent::Lost {
1856 split: SplitId::new("a").unwrap(),
1857 }]);
1858 let event = poll(&mut d, &mut s);
1859 let SourceEvent::LanesRevoked { lanes, barrier } = event else {
1860 panic!("expected revoke, got {event:?}");
1861 };
1862 assert_eq!(lanes, vec![LaneId(0)]);
1863 assert_eq!(barrier.remaining(), 1, "one party per revoked lane");
1864 assert_eq!(s.closed, vec!["a"], "fetcher detached on loss");
1865 assert_eq!(d.assignments().len(), 1);
1866 }
1867
1868 #[test]
1869 fn late_drain_commit_after_loss_is_skipped() {
1870 let script = Script::default();
1871 let mut d = driver(&script);
1872 let mut s = TestSource::default();
1873 script.push(vec![gained("a", 1, None)]);
1874 poll(&mut d, &mut s);
1875 let partition = s.opened[0].3;
1876
1877 script.push(vec![CoordinationEvent::Lost {
1878 split: SplitId::new("a").unwrap(),
1879 }]);
1880 poll(&mut d, &mut s);
1881
1882 d.commit(&mut s, &[(partition, 42)]).unwrap();
1884 assert!(s.encoded.is_empty(), "retired tenancy must not encode");
1885 assert!(script.commits().is_empty(), "and must not commit");
1886 }
1887
1888 #[test]
1889 fn fenced_commit_quarantines_the_tenancy_and_never_respawns_it() {
1890 let script = Script::default();
1891 let mut d = driver(&script);
1892 let mut s = TestSource::default();
1893 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
1894 poll(&mut d, &mut s);
1895 let a_partition = s.opened[0].3;
1896 let b_partition = s.opened[1].3;
1897
1898 script.fail_next_commit("a", CoordinationErrorKind::Fenced);
1899 d.commit(&mut s, &[(a_partition, 10), (b_partition, 20)])
1900 .unwrap();
1901 assert_eq!(script.commits().len(), 1);
1903 assert_eq!(script.commits()[0].0.as_str(), "b");
1904 assert_eq!(s.closed, vec!["a"]);
1905
1906 let event = poll(&mut d, &mut s);
1908 assert!(
1909 matches!(event, SourceEvent::LanesRevoked { ref lanes, .. } if lanes[..] == [LaneId(0)])
1910 );
1911
1912 s.encoded.clear();
1914 d.commit(&mut s, &[(a_partition, 11)]).unwrap();
1915 assert!(s.encoded.is_empty());
1916
1917 script.push(vec![
1921 CoordinationEvent::Lost {
1922 split: SplitId::new("a").unwrap(),
1923 },
1924 gained("a", 3, Some(10)),
1925 ]);
1926 let event = poll(&mut d, &mut s);
1927 let SourceEvent::LanesAdded(lanes) = event else {
1928 panic!("expected an added lane for the re-gain, got {event:?}");
1929 };
1930 assert_eq!(lanes.len(), 1, "only the fresh tenancy's lane");
1931 assert_eq!(s.closed, vec!["a"], "b's fetcher was never detached");
1932 let a_again = s.opened.last().unwrap();
1933 assert_eq!(a_again.0, "a");
1934 assert_eq!(a_again.4, 3, "fresh tenancy under the new epoch");
1935 assert_ne!(a_again.3, a_partition, "fresh partition — no reuse");
1936 assert_eq!(a_again.2, LaneId(2), "fresh lane id — never reused");
1937 }
1938
1939 #[test]
1940 fn lost_then_regained_in_one_batch_is_a_clean_tenancy_swap() {
1941 let script = Script::default();
1942 let mut d = driver(&script);
1943 let mut s = TestSource::default();
1944 script.push(vec![gained("a", 1, None)]);
1945 poll(&mut d, &mut s);
1946 let first_partition = s.opened[0].3;
1947
1948 script.push(vec![
1949 CoordinationEvent::Lost {
1950 split: SplitId::new("a").unwrap(),
1951 },
1952 gained("a", 2, Some(5)),
1953 ]);
1954 let event = poll(&mut d, &mut s);
1956 assert!(matches!(event, SourceEvent::LanesRevoked { .. }));
1957 let event = poll(&mut d, &mut s);
1958 assert!(matches!(event, SourceEvent::LanesAdded(ref l) if l.len() == 1));
1959 let reopened = s.opened.last().unwrap();
1960 assert_eq!(reopened.4, 2);
1961 assert_eq!(reopened.1, Some(5), "resume from the carried progress");
1962 assert_ne!(reopened.3, first_partition);
1963 assert_eq!(reopened.2, LaneId(1), "lane ids are never recycled");
1964 }
1965
1966 #[test]
1967 fn retryable_commit_defers_and_recommits_idempotently() {
1968 let script = Script::default();
1969 let mut d = driver(&script);
1970 let mut s = TestSource::default();
1971 script.push(vec![gained("a", 1, None)]);
1972 poll(&mut d, &mut s);
1973 let partition = s.opened[0].3;
1974
1975 script.fail_next_commit("a", CoordinationErrorKind::Retryable);
1976 let err = d
1977 .commit(&mut s, &[(partition, 10)])
1978 .expect_err("a deferred split fails the commit");
1979 assert!(
1980 matches!(
1981 &err,
1982 SourceError::Client {
1983 class: ErrorClass::Retryable,
1984 ..
1985 }
1986 ),
1987 "{err:?}"
1988 );
1989 assert!(script.commits().is_empty(), "deferred, not written");
1990 assert_eq!(
1991 d.tenancies[&partition]
1992 .progress
1993 .as_ref()
1994 .map(|p| p.watermark),
1995 Some(10),
1996 "the resume cache holds the deferred progress"
1997 );
1998
1999 poll(&mut d, &mut s);
2002 assert!(script.commits().is_empty(), "a tick recommitted nothing");
2003
2004 d.commit(&mut s, &[(partition, 12)]).unwrap();
2006 assert_eq!(script.commits().len(), 1);
2007 assert_eq!(script.commits()[0].1.watermark, 12);
2008 }
2009
2010 #[test]
2013 fn a_deferred_split_fails_the_commit_after_every_split_is_sent() {
2014 let script = Script::default();
2015 let mut d = driver(&script);
2016 let mut s = TestSource::default();
2017 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
2018 poll(&mut d, &mut s);
2019 let pa = s.opened[0].3;
2020 let pb = s.opened[1].3;
2021
2022 script.fail_next_commit("a", CoordinationErrorKind::Retryable);
2023 let result = d.commit(&mut s, &[(pa, 10), (pb, 20)]);
2024
2025 assert!(
2026 matches!(
2027 &result,
2028 Err(SourceError::Client {
2029 class: ErrorClass::Retryable,
2030 ..
2031 })
2032 ),
2033 "{result:?}"
2034 );
2035 let sent: Vec<_> = script
2036 .commits()
2037 .into_iter()
2038 .map(|(split, _)| split)
2039 .collect();
2040 assert_eq!(sent, vec![SplitId::new("b").unwrap()]);
2041 }
2042
2043 #[test]
2046 fn a_stop_during_a_commit_sends_no_later_split() {
2047 let script = Script::default();
2048 let mut d = driver(&script);
2049 let mut s = TestSource::default();
2050 let flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
2051 d.set_stop(crate::source::StopSignal::new(Arc::clone(&flag)));
2052 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
2053 poll(&mut d, &mut s);
2054 let pa = s.opened[0].3;
2055 let pb = s.opened[1].3;
2056 script.stop_on_commit("a", &flag);
2057
2058 let result = d.commit(&mut s, &[(pa, 10), (pb, 20)]);
2059
2060 assert!(
2061 matches!(
2062 &result,
2063 Err(SourceError::Client {
2064 class: ErrorClass::Retryable,
2065 ..
2066 })
2067 ),
2068 "{result:?}"
2069 );
2070 let sent: Vec<_> = script
2071 .commits()
2072 .into_iter()
2073 .map(|(split, _)| split)
2074 .collect();
2075 assert_eq!(sent, vec![SplitId::new("a").unwrap()]);
2076 }
2077
2078 #[test]
2080 fn commit_final_sends_every_split_while_the_stop_is_set() {
2081 let script = Script::default();
2082 let mut d = driver(&script);
2083 let mut s = TestSource::default();
2084 d.set_stop(crate::source::StopSignal::new(Arc::new(
2085 std::sync::atomic::AtomicBool::new(true),
2086 )));
2087 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
2088 poll(&mut d, &mut s);
2089 let pa = s.opened[0].3;
2090 let pb = s.opened[1].3;
2091
2092 let unstored = d.commit_final(&mut s, &[(pa, 10), (pb, 20)]).unwrap();
2093
2094 assert!(unstored.is_empty(), "{unstored:?}");
2095 assert_eq!(script.commits().len(), 2);
2096 }
2097
2098 #[test]
2100 fn set_stop_reaches_the_coordinator() {
2101 let script = Script::default();
2102 let mut d = driver(&script);
2103 let flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
2104 d.set_stop(crate::source::StopSignal::new(Arc::clone(&flag)));
2105 flag.store(true, std::sync::atomic::Ordering::SeqCst);
2106 assert!(script.stop().is_some_and(|stop| stop.is_set()));
2107 }
2108
2109 #[test]
2112 fn a_report_refused_at_the_stop_stays_queued() {
2113 let script = Script::default();
2114 let mut d = driver(&script);
2115 let mut s = TestSource::default();
2116 d.set_stop(crate::source::StopSignal::new(Arc::new(
2117 std::sync::atomic::AtomicBool::new(true),
2118 )));
2119 script.push(vec![gained("a", 1, None)]);
2120 poll(&mut d, &mut s);
2121 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2122
2123 d.fail(&mut s, &SplitId::new("a").unwrap(), LeaseEpoch(1), "poison")
2124 .unwrap();
2125 assert_eq!(d.assignments().len(), 1, "the tenancy stays live");
2126
2127 poll(&mut d, &mut s);
2128 assert_eq!(script.fails().len(), 2, "the report is offered again");
2129 }
2130
2131 #[test]
2132 fn completion_sweep_commits_terminal_progress_and_frees_the_lane() {
2133 let script = Script::default();
2134 let mut d = driver(&script);
2135 let mut s = TestSource::default();
2136 script.push(vec![gained("a", 1, None)]);
2137 poll(&mut d, &mut s);
2138
2139 s.sweeps
2140 .borrow_mut()
2141 .insert("a".into(), SplitProgress::completed(9, vec![]));
2142 let event = poll(&mut d, &mut s);
2146 assert!(
2147 matches!(event, SourceEvent::LanesRetired { ref lanes } if lanes[..] == [LaneId(0)]),
2148 "completed lanes retire without a drain barrier, got {event:?}"
2149 );
2150 assert_eq!(script.commits().len(), 1);
2151 assert!(script.commits()[0].1.completed);
2152
2153 script.push(vec![gained("b", 1, None)]);
2155 poll(&mut d, &mut s);
2156 let b_partition = s.opened.last().unwrap().3;
2157 s.complete_at.insert("b".into(), 20);
2158 d.commit(&mut s, &[(b_partition, 20)]).unwrap();
2159 assert!(script.commits().last().unwrap().1.completed);
2160 let event = poll(&mut d, &mut s);
2161 assert!(matches!(event, SourceEvent::LanesRetired { .. }));
2162 }
2163
2164 #[test]
2165 fn standby_with_zero_splits_drains_on_all_complete() {
2166 let script = Script::default();
2167 let mut d = driver(&script);
2168 let mut s = TestSource::default();
2169
2170 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2171 script.push(vec![CoordinationEvent::AllComplete]);
2172 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Drained));
2173 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Drained));
2175 }
2176
2177 #[test]
2178 fn stalled_is_fatal_by_default_and_drains_when_configured() {
2179 let script = Script::default();
2180 let mut d = driver(&script);
2181 let mut s = TestSource::default();
2182 script.push(vec![CoordinationEvent::Stalled {
2183 completed: 7,
2184 quarantined: 1,
2185 }]);
2186 let err = d.poll_events(&mut s, Duration::ZERO).unwrap_err();
2188 assert!(err.to_string().contains("quarantined"), "{err}");
2189
2190 let script = Script::default();
2191 let mut d = CoordinationDriver::new(Box::new(ScriptedCoordinator(script.clone())))
2192 .stall_drains(true);
2193 let _: SourceEvent<StubLane> = d.start(Box::new(NoopPlanner)).unwrap();
2194 script.push(vec![CoordinationEvent::Stalled {
2195 completed: 7,
2196 quarantined: 1,
2197 }]);
2198 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Drained));
2199 }
2200
2201 #[test]
2202 fn fail_reports_poison_and_retires_the_lane() {
2203 let script = Script::default();
2204 let mut d = driver(&script);
2205 let mut s = TestSource::default();
2206 script.push(vec![gained("a", 1, None)]);
2207 poll(&mut d, &mut s);
2208
2209 d.fail(
2210 &mut s,
2211 &SplitId::new("a").unwrap(),
2212 LeaseEpoch(1),
2213 "undecodable object",
2214 )
2215 .unwrap();
2216 assert_eq!(script.fails().len(), 1);
2217 assert_eq!(s.closed, vec!["a"]);
2218 let event = poll(&mut d, &mut s);
2219 assert!(matches!(event, SourceEvent::LanesRevoked { .. }));
2220 d.fail(&mut s, &SplitId::new("a").unwrap(), LeaseEpoch(1), "again")
2222 .unwrap();
2223 assert_eq!(script.fails().len(), 1);
2224 }
2225
2226 #[test]
2227 fn a_retryable_failure_report_is_retried_until_it_lands() {
2228 let script = Script::default();
2229 let mut d = driver(&script);
2230 let mut s = TestSource::default();
2231 script.push(vec![gained("a", 4, None)]);
2232 poll(&mut d, &mut s);
2233 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2234
2235 d.fail(
2236 &mut s,
2237 &SplitId::new("a").unwrap(),
2238 LeaseEpoch(4),
2239 "undecodable object",
2240 )
2241 .unwrap();
2242 assert_eq!(script.fails().len(), 1);
2243 assert!(
2244 s.closed.is_empty(),
2245 "the split is held until the report lands"
2246 );
2247
2248 let event = poll(&mut d, &mut s);
2249 assert_eq!(script.fails().len(), 2);
2250 assert_eq!(
2251 script.fails()[1].1,
2252 LeaseEpoch(4),
2253 "the re-offer keeps the epoch"
2254 );
2255 assert_eq!(s.closed, vec!["a"]);
2256 assert!(matches!(event, SourceEvent::LanesRevoked { .. }));
2257 poll(&mut d, &mut s);
2258 assert_eq!(script.fails().len(), 2);
2259 }
2260
2261 #[test]
2262 fn a_queued_failure_report_is_dropped_when_the_split_is_lost() {
2263 let script = Script::default();
2264 let mut d = driver(&script);
2265 let mut s = TestSource::default();
2266 script.push(vec![gained("a", 1, None)]);
2267 poll(&mut d, &mut s);
2268 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2269 d.fail(
2270 &mut s,
2271 &SplitId::new("a").unwrap(),
2272 LeaseEpoch(1),
2273 "undecodable object",
2274 )
2275 .unwrap();
2276
2277 script.push(vec![CoordinationEvent::Lost {
2279 split: split("a").id,
2280 }]);
2281 script.push(vec![gained("a", 2, None)]);
2282 poll(&mut d, &mut s);
2283 poll(&mut d, &mut s);
2284 assert_eq!(script.fails().len(), 1, "a stale report is not re-offered");
2285 assert_eq!(d.assignments().len(), 1, "the regained split stays live");
2286 }
2287
2288 #[test]
2289 fn a_fatal_failure_report_is_returned() {
2290 let script = Script::default();
2291 let mut d = driver(&script);
2292 let mut s = TestSource::default();
2293 script.push(vec![gained("a", 1, None)]);
2294 poll(&mut d, &mut s);
2295 script.fail_next_report("a", CoordinationErrorKind::Fatal);
2296
2297 let err = d
2298 .fail(
2299 &mut s,
2300 &SplitId::new("a").unwrap(),
2301 LeaseEpoch(1),
2302 "undecodable object",
2303 )
2304 .unwrap_err();
2305 assert!(is_fatal(&err), "{err}");
2306 }
2307
2308 #[test]
2309 fn release_departs_with_every_live_split() {
2310 let script = Script::default();
2311 let mut d = driver(&script);
2312 let mut s = TestSource::default();
2313 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
2314 poll(&mut d, &mut s);
2315
2316 d.release();
2317 let departed = script.departed().expect("the driver departs");
2318 assert_eq!(departed.len(), 2);
2319 assert!(departed.iter().any(|s| s.as_str() == "a"));
2320 assert!(departed.iter().any(|s| s.as_str() == "b"));
2321 }
2322
2323 #[test]
2326 fn release_departs_with_nothing_held() {
2327 let script = Script::default();
2328 let mut d = driver(&script);
2329 d.release();
2330 assert_eq!(script.departed(), Some(Vec::new()));
2331 }
2332
2333 #[test]
2334 fn resume_validation_rejects_drifted_progress() {
2335 let script = Script::default();
2336 let mut d = driver(&script);
2337 let mut s = rejecting(&["a"]);
2338 script.push(vec![gained("a", 1, Some(7))]);
2339 let err = d.poll_events(&mut s, Duration::ZERO).unwrap_err();
2340 assert!(err.to_string().contains("resume drift"), "{err}");
2341
2342 assert!(s.opened.is_empty());
2345 let fails = script.fails();
2346 assert_eq!(fails.len(), 1);
2347 assert_eq!(fails[0].0.as_str(), "a");
2348 assert!(fails[0].2.contains("resume drift on a"), "{}", fails[0].2);
2349 }
2350
2351 #[test]
2352 fn a_refused_resume_leaves_the_rest_of_the_batch_applied() {
2353 let script = Script::default();
2354 let mut d = driver(&script);
2355 let mut s = rejecting(&["a"]);
2356 script.push(vec![gained("a", 1, Some(7)), gained("b", 1, Some(3))]);
2359 let err = d.poll_events(&mut s, Duration::ZERO).unwrap_err();
2360 assert!(err.to_string().contains("resume drift on a"), "{err}");
2361 assert_eq!(script.fails().len(), 1);
2362
2363 let event = poll(&mut d, &mut s);
2365 let SourceEvent::LanesAdded(lanes) = event else {
2366 panic!("expected the sound split to open");
2367 };
2368 assert_eq!(lanes.len(), 1);
2369 assert_eq!(s.opened.len(), 1);
2370 assert_eq!(s.opened[0].0, "b");
2371 assert_eq!(s.opened[0].1, Some(3));
2372 }
2373
2374 #[test]
2375 fn a_failed_poison_report_does_not_replace_the_rejection() {
2376 let script = Script::default();
2377 let mut d = driver(&script);
2378 let mut s = rejecting(&["a"]);
2379 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2380 script.push(vec![gained("a", 1, Some(7))]);
2381
2382 let err = d.poll_events(&mut s, Duration::ZERO).unwrap_err();
2385 assert!(err.to_string().contains("resume drift on a"), "{err}");
2386 assert_eq!(script.fails().len(), 1);
2387 }
2388
2389 #[test]
2390 fn a_refused_poison_report_is_retried_until_it_lands() {
2391 let script = Script::default();
2392 let mut d = driver(&script);
2393 let mut s = rejecting(&["a"]);
2394 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2395 script.push(vec![gained("a", 3, Some(7))]);
2396 d.poll_events(&mut s, Duration::ZERO).unwrap_err();
2397 assert_eq!(script.fails().len(), 1);
2398
2399 poll(&mut d, &mut s);
2402 assert_eq!(script.fails().len(), 2);
2403 poll(&mut d, &mut s);
2404 let fails = script.fails();
2405 assert_eq!(fails.len(), 2);
2406 assert!(
2407 fails.iter().all(|(_, epoch, _)| *epoch == LeaseEpoch(3)),
2408 "{fails:?}"
2409 );
2410 }
2411
2412 #[test]
2415 fn a_fenced_report_for_a_rejected_gain_keeps_the_tenancy_regained_without_a_loss() {
2416 let script = Script::default();
2417 let mut d = driver(&script);
2418 let mut s = rejecting(&["a"]);
2419 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2422 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2423 script.fail_next_report("a", CoordinationErrorKind::Fenced);
2424 script.push(vec![gained("a", 1, Some(7))]);
2425 d.poll_events(&mut s, Duration::ZERO).unwrap_err();
2426 s.reject_resume.clear();
2427 script.push(vec![gained("a", 2, Some(7))]);
2428 for _ in 0..4 {
2429 let _ = d.poll_events(&mut s, Duration::ZERO);
2430 }
2431 assert_eq!(script.fails().len(), 3);
2432 assert_eq!(script.fails()[2].1, LeaseEpoch(1));
2433 assert_eq!(
2434 d.assignments().len(),
2435 1,
2436 "the tenancy at epoch 2 stays live"
2437 );
2438 }
2439
2440 #[test]
2443 fn fail_for_an_earlier_tenancy_leaves_the_current_one_live() {
2444 let script = Script::default();
2445 let mut d = driver(&script);
2446 let mut s = TestSource::default();
2447 script.push(vec![gained("a", 1, None)]);
2448 poll(&mut d, &mut s);
2449 script.push(vec![CoordinationEvent::Lost {
2450 split: split("a").id,
2451 }]);
2452 poll(&mut d, &mut s);
2453 script.push(vec![gained("a", 2, None)]);
2454 poll(&mut d, &mut s);
2455 let closed = s.closed.clone();
2456 assert_eq!(closed, vec!["a"]);
2457
2458 d.fail(
2459 &mut s,
2460 &SplitId::new("a").unwrap(),
2461 LeaseEpoch(1),
2462 "undecodable object",
2463 )
2464 .unwrap();
2465 assert!(script.fails().is_empty(), "{:?}", script.fails());
2466 assert_eq!(s.closed, closed);
2467 assert_eq!(d.assignments().len(), 1);
2468 }
2469
2470 #[test]
2473 fn stale_rejected_gain_report_does_not_fail_a_regained_split() {
2474 let script = Script::default();
2475 let mut d = driver(&script);
2476 let mut s = rejecting(&["a"]);
2477 for _ in 0..3 {
2478 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2479 }
2480 script.push(vec![gained("a", 1, Some(7))]);
2481 d.poll_events(&mut s, Duration::ZERO).unwrap_err();
2482 s.reject_resume.clear();
2483 script.push(vec![CoordinationEvent::Lost {
2484 split: split("a").id,
2485 }]);
2486 script.push(vec![gained("a", 2, Some(7))]);
2487 for _ in 0..5 {
2488 let _ = d.poll_events(&mut s, Duration::ZERO);
2489 }
2490 assert_eq!(
2491 script.fails().len(),
2492 1,
2493 "the stale report is not re-offered"
2494 );
2495 assert_eq!(d.assignments().len(), 1);
2496 }
2497
2498 #[test]
2501 fn stale_rejected_gain_report_is_not_offered_when_the_loss_and_regain_share_a_batch() {
2502 let script = Script::default();
2503 let mut d = driver(&script);
2504 let mut s = rejecting(&["a"]);
2505 for _ in 0..3 {
2506 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2507 }
2508 script.push(vec![gained("a", 1, Some(7))]);
2509 d.poll_events(&mut s, Duration::ZERO).unwrap_err();
2510 s.reject_resume.clear();
2511 script.push(vec![
2512 CoordinationEvent::Lost {
2513 split: split("a").id,
2514 },
2515 gained("a", 2, Some(7)),
2516 ]);
2517 for _ in 0..4 {
2518 let _ = d.poll_events(&mut s, Duration::ZERO);
2519 }
2520 assert_eq!(
2521 script.fails().len(),
2522 1,
2523 "the stale report is not re-offered"
2524 );
2525 assert_eq!(d.assignments().len(), 1);
2526 }
2527
2528 #[test]
2531 fn rejected_gain_report_queued_in_the_batch_that_loses_the_split_is_dropped() {
2532 let script = Script::default();
2533 let mut d = driver(&script);
2534 let mut s = rejecting(&["a"]);
2535 for _ in 0..3 {
2536 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2537 }
2538 script.push(vec![
2539 gained("a", 1, Some(7)),
2540 CoordinationEvent::Lost {
2541 split: split("a").id,
2542 },
2543 gained("a", 2, None),
2544 ]);
2545 let _ = d.poll_events(&mut s, Duration::ZERO);
2546 for _ in 0..4 {
2547 let _ = d.poll_events(&mut s, Duration::ZERO);
2548 }
2549 assert_eq!(
2550 script.fails().len(),
2551 1,
2552 "the stale report is not re-offered"
2553 );
2554 assert_eq!(d.assignments().len(), 1);
2555 }
2556
2557 #[test]
2560 fn rejected_gain_report_queued_in_the_batch_that_quarantines_the_split_is_dropped() {
2561 let script = Script::default();
2562 let mut d = driver(&script);
2563 let mut s = rejecting(&["a"]);
2564 for _ in 0..3 {
2565 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2566 }
2567 script.push(vec![
2568 gained("a", 1, Some(7)),
2569 CoordinationEvent::Quarantined {
2570 split: split("a").id,
2571 attempts: 3,
2572 },
2573 ]);
2574 let _ = d.poll_events(&mut s, Duration::ZERO);
2575 for _ in 0..4 {
2576 let _ = d.poll_events(&mut s, Duration::ZERO);
2577 }
2578 assert_eq!(
2579 script.fails().len(),
2580 1,
2581 "the stale report is not re-offered"
2582 );
2583 }
2584
2585 #[test]
2587 fn rejected_gain_report_is_dropped_when_the_split_is_quarantined() {
2588 let script = Script::default();
2589 let mut d = driver(&script);
2590 let mut s = rejecting(&["a"]);
2591 for _ in 0..3 {
2592 script.fail_next_report("a", CoordinationErrorKind::Retryable);
2593 }
2594 script.push(vec![gained("a", 1, Some(7))]);
2595 d.poll_events(&mut s, Duration::ZERO).unwrap_err();
2596 script.push(vec![CoordinationEvent::Quarantined {
2597 split: split("a").id,
2598 attempts: 3,
2599 }]);
2600 for _ in 0..4 {
2601 let _ = d.poll_events(&mut s, Duration::ZERO);
2602 }
2603 assert_eq!(
2604 script.fails().len(),
2605 1,
2606 "the stale report is not re-offered"
2607 );
2608 }
2609
2610 #[test]
2611 fn a_fatal_rejection_survives_an_earlier_retryable_one() {
2612 let script = Script::default();
2613 let mut d = driver(&script);
2614 let mut s = TestSource {
2615 reject_resume: HashMap::from([
2616 ("a".to_string(), ErrorClass::Retryable),
2617 ("b".to_string(), ErrorClass::Fatal),
2618 ]),
2619 ..TestSource::default()
2620 };
2621 script.push(vec![gained("a", 1, Some(7)), gained("b", 1, Some(3))]);
2622
2623 let err = d.poll_events(&mut s, Duration::ZERO).unwrap_err();
2626 assert!(is_fatal(&err), "{err}");
2627 assert!(err.to_string().contains("resume drift on b"), "{err}");
2628 assert_eq!(script.fails().len(), 2);
2629 }
2630
2631 #[test]
2636 fn a_drain_keeps_the_tenancy_commit_eligible_until_the_final_commit() {
2637 let script = Script::default();
2641 let mut d = driver(&script);
2642 let mut s = TestSource {
2643 accept_revoke: HashSet::from(["a".to_string()]),
2644 ..TestSource::default()
2645 };
2646 script.push(vec![gained("a", 1, None)]);
2647 poll(&mut d, &mut s);
2648 let partition = s.opened[0].3;
2649
2650 script.push(vec![CoordinationEvent::RevokeRequested {
2653 split: SplitId::new("a").unwrap(),
2654 }]);
2655 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2656 assert_eq!(
2657 s.begin_revoke_calls,
2658 ["a"],
2659 "the source was asked to stop intake"
2660 );
2661
2662 d.commit(&mut s, &[(partition, 42)]).unwrap();
2664 assert_eq!(s.encoded, vec![("a".to_string(), 42)]);
2665 assert_eq!(script.commits().len(), 1);
2666 assert_eq!(script.commits()[0].0.as_str(), "a");
2667 assert!(
2668 !script.commits()[0].1.completed,
2669 "a revocation never completes the split"
2670 );
2671 assert!(
2672 script.released_drained().is_empty(),
2673 "not released while the drain is still in flight"
2674 );
2675 }
2676
2677 #[test]
2678 fn a_completed_drain_releases_exactly_one_split_and_retires_barrierless() {
2679 let script = Script::default();
2680 let mut d = driver(&script);
2681 let mut s = TestSource {
2682 accept_revoke: HashSet::from(["a".to_string()]),
2683 ..TestSource::default()
2684 };
2685 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
2686 poll(&mut d, &mut s);
2687
2688 s.ready_progress
2691 .borrow_mut()
2692 .insert("a".into(), SplitProgress::new(50, vec![]));
2693 script.push(vec![CoordinationEvent::RevokeRequested {
2694 split: SplitId::new("a").unwrap(),
2695 }]);
2696
2697 let event = poll(&mut d, &mut s);
2700 let SourceEvent::LanesRetired { lanes } = event else {
2701 panic!("a cooperative revocation must retire barrier-less, got {event:?}");
2702 };
2703 assert_eq!(lanes, vec![LaneId(0)], "only a's lane leaves");
2704
2705 assert_eq!(
2706 script.released_drained(),
2707 vec![SplitId::new("a").unwrap()],
2708 "exactly one split, released via the revocation path"
2709 );
2710 assert!(script.released().is_empty(), "not a plain hand-back");
2711 let last = script.commits().last().cloned().unwrap();
2713 assert_eq!(last.0.as_str(), "a");
2714 assert_eq!(last.1.watermark, 50);
2715 assert!(!last.1.completed, "drain commits never complete the split");
2716
2717 assert_eq!(s.closed, vec!["a"], "b's fetcher stays attached");
2719 assert!(
2720 d.assignments().iter().any(|(id, _)| id.as_str() == "b"),
2721 "b is still owned"
2722 );
2723 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2724 }
2725
2726 #[test]
2727 fn a_fenced_final_commit_aborts_the_drain_into_a_revoke() {
2728 let script = Script::default();
2729 let mut d = driver(&script);
2730 let mut s = TestSource {
2731 accept_revoke: HashSet::from(["a".to_string()]),
2732 ..TestSource::default()
2733 };
2734 script.push(vec![gained("a", 1, None)]);
2735 poll(&mut d, &mut s);
2736 let partition = s.opened[0].3;
2737
2738 s.ready_progress
2741 .borrow_mut()
2742 .insert("a".into(), SplitProgress::new(50, vec![]));
2743 script.fail_next_commit("a", CoordinationErrorKind::Fenced);
2744 script.push(vec![CoordinationEvent::RevokeRequested {
2745 split: SplitId::new("a").unwrap(),
2746 }]);
2747
2748 let event = poll(&mut d, &mut s);
2749 let SourceEvent::LanesRevoked { lanes, barrier } = event else {
2750 panic!("a fenced drain must abort into a revoke, got {event:?}");
2751 };
2752 assert_eq!(lanes, vec![LaneId(0)]);
2753 assert_eq!(barrier.remaining(), 1, "one party per revoked lane");
2754 assert!(
2755 script.released_drained().is_empty(),
2756 "a fenced drain never releases"
2757 );
2758 assert!(
2759 script.commits().is_empty(),
2760 "the fenced final commit wrote nothing"
2761 );
2762 assert_eq!(s.closed, vec!["a"], "the fetcher was detached on the abort");
2763
2764 d.commit(&mut s, &[(partition, 60)]).unwrap();
2766 assert!(
2767 s.encoded.is_empty(),
2768 "a retired-fenced tenancy must not encode"
2769 );
2770 }
2771
2772 #[test]
2773 fn a_retryable_final_commit_keeps_the_drain_pending() {
2774 let script = Script::default();
2775 let mut d = driver(&script);
2776 let mut s = TestSource {
2777 accept_revoke: HashSet::from(["a".to_string()]),
2778 ..TestSource::default()
2779 };
2780 script.push(vec![gained("a", 1, None)]);
2781 poll(&mut d, &mut s);
2782
2783 s.ready_progress
2785 .borrow_mut()
2786 .insert("a".into(), SplitProgress::new(50, vec![]));
2787 script.fail_next_commit("a", CoordinationErrorKind::Retryable);
2788 script.push(vec![CoordinationEvent::RevokeRequested {
2789 split: SplitId::new("a").unwrap(),
2790 }]);
2791
2792 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2795 assert!(script.commits().is_empty(), "deferred, not written");
2796 assert!(script.released_drained().is_empty());
2797 assert!(
2798 d.assignments().iter().any(|(id, _)| id.as_str() == "a"),
2799 "still owned while the final commit retries"
2800 );
2801
2802 let event = poll(&mut d, &mut s);
2805 let SourceEvent::LanesRetired { lanes } = event else {
2806 panic!("the retried drain must finally retire, got {event:?}");
2807 };
2808 assert_eq!(lanes, vec![LaneId(0)]);
2809 assert_eq!(script.commits().len(), 1);
2810 assert_eq!(script.commits()[0].1.watermark, 50);
2811 assert_eq!(script.released_drained(), vec![SplitId::new("a").unwrap()]);
2812 }
2813
2814 #[test]
2815 fn a_source_that_cannot_stop_intake_declines_the_revoke() {
2816 let script = Script::default();
2817 let mut d = driver(&script);
2818 let mut s = TestSource::default();
2820 script.push(vec![gained("a", 1, None)]);
2821 poll(&mut d, &mut s);
2822 let partition = s.opened[0].3;
2823
2824 script.push(vec![CoordinationEvent::RevokeRequested {
2825 split: SplitId::new("a").unwrap(),
2826 }]);
2827 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2828
2829 assert_eq!(s.begin_revoke_calls, ["a"]);
2832 assert!(
2833 s.drain_ready_calls.is_empty(),
2834 "a declined split never drains"
2835 );
2836 assert!(script.released_drained().is_empty());
2837
2838 d.commit(&mut s, &[(partition, 30)]).unwrap();
2840 assert_eq!(s.encoded, vec![("a".to_string(), 30)]);
2841 assert_eq!(script.commits().len(), 1);
2842 }
2843
2844 #[test]
2845 fn a_revoke_request_for_an_unheld_split_is_ignored() {
2846 let script = Script::default();
2847 let mut d = driver(&script);
2848 let mut s = TestSource {
2849 accept_revoke: HashSet::from(["a".to_string(), "ghost".to_string()]),
2852 ..TestSource::default()
2853 };
2854 script.push(vec![gained("a", 1, None)]);
2855 poll(&mut d, &mut s);
2856
2857 script.push(vec![CoordinationEvent::RevokeRequested {
2858 split: SplitId::new("ghost").unwrap(),
2859 }]);
2860 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2861 assert!(
2862 s.begin_revoke_calls.is_empty(),
2863 "an unheld split must not consult the source"
2864 );
2865 assert!(
2866 d.assignments().iter().any(|(id, _)| id.as_str() == "a"),
2867 "the held split is untouched"
2868 );
2869 }
2870
2871 #[test]
2872 fn a_source_that_declines_feeds_the_decline_back() {
2873 let script = Script::default();
2877 let mut d = driver(&script);
2878 let mut s = TestSource::default();
2880 script.push(vec![gained("a", 1, None)]);
2881 poll(&mut d, &mut s);
2882 let partition = s.opened[0].3;
2883
2884 script.push(vec![CoordinationEvent::RevokeRequested {
2885 split: SplitId::new("a").unwrap(),
2886 }]);
2887 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2888
2889 assert_eq!(s.begin_revoke_calls, ["a"]);
2892 assert_eq!(
2893 script.declined(),
2894 vec![SplitId::new("a").unwrap()],
2895 "the source's refusal must reach the backend, once"
2896 );
2897
2898 d.commit(&mut s, &[(partition, 30)]).unwrap();
2900 assert_eq!(s.encoded, vec![("a".to_string(), 30)]);
2901 assert_eq!(script.commits().len(), 1);
2902 assert_eq!(script.commits()[0].0.as_str(), "a");
2903 }
2904
2905 #[test]
2906 fn a_repeated_revoke_request_mid_drain_is_not_declined() {
2907 let script = Script::default();
2912 let mut d = driver(&script);
2913 let mut s = TestSource {
2914 accept_revoke: HashSet::from(["a".to_string()]),
2915 ..TestSource::default()
2916 };
2917 script.push(vec![gained("a", 1, None)]);
2918 poll(&mut d, &mut s);
2919
2920 script.push(vec![CoordinationEvent::RevokeRequested {
2923 split: SplitId::new("a").unwrap(),
2924 }]);
2925 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2926 assert_eq!(s.begin_revoke_calls, ["a"]);
2927
2928 script.push(vec![CoordinationEvent::RevokeRequested {
2930 split: SplitId::new("a").unwrap(),
2931 }]);
2932 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
2933 assert_eq!(
2934 s.begin_revoke_calls,
2935 ["a"],
2936 "the source must not be asked to stop intake it has already stopped"
2937 );
2938 assert!(
2939 script.declined().is_empty(),
2940 "a drain already in flight satisfies the request; declining it would force the release"
2941 );
2942
2943 s.ready_progress
2945 .borrow_mut()
2946 .insert("a".into(), SplitProgress::new(50, vec![]));
2947 let event = poll(&mut d, &mut s);
2948 let SourceEvent::LanesRetired { lanes } = event else {
2949 panic!("the drain must still retire, got {event:?}");
2950 };
2951 assert_eq!(lanes, vec![LaneId(0)]);
2952 assert_eq!(script.released_drained(), vec![SplitId::new("a").unwrap()]);
2953 }
2954
2955 #[test]
2956 fn an_unopened_tenancy_declines_without_asking_the_source() {
2957 let script = Script::default();
2962 let mut d = driver(&script);
2963 let mut s = TestSource {
2966 accept_revoke: HashSet::from(["a".to_string()]),
2967 ..TestSource::default()
2968 };
2969
2970 script.push(vec![
2971 gained("a", 1, None),
2972 CoordinationEvent::RevokeRequested {
2973 split: SplitId::new("a").unwrap(),
2974 },
2975 ]);
2976 let event = poll(&mut d, &mut s);
2979 let SourceEvent::LanesAdded(lanes) = event else {
2980 panic!("the split must still open after the early decline, got {event:?}");
2981 };
2982 assert_eq!(lanes.len(), 1);
2983
2984 assert!(
2985 s.begin_revoke_calls.is_empty(),
2986 "a not-yet-opened tenancy must never be asked to stop intake"
2987 );
2988 assert_eq!(
2989 script.declined(),
2990 vec![SplitId::new("a").unwrap()],
2991 "the premature request is declined back to the backend"
2992 );
2993
2994 let partition = s.opened[0].3;
2996 d.commit(&mut s, &[(partition, 25)]).unwrap();
2997 assert_eq!(s.encoded, vec![("a".to_string(), 25)]);
2998 assert_eq!(script.commits().len(), 1);
2999 assert!(script.released_drained().is_empty());
3001 }
3002
3003 #[test]
3004 fn a_completed_progress_during_a_drain_is_never_terminal() {
3005 let script = Script::default();
3008 let mut d = driver(&script);
3009 let mut s = TestSource {
3010 accept_revoke: HashSet::from(["a".to_string()]),
3011 ..TestSource::default()
3012 };
3013 script.push(vec![gained("a", 1, None)]);
3014 poll(&mut d, &mut s);
3015 let partition = s.opened[0].3;
3016
3017 script.push(vec![CoordinationEvent::RevokeRequested {
3020 split: SplitId::new("a").unwrap(),
3021 }]);
3022 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
3023 assert_eq!(s.begin_revoke_calls, ["a"]);
3024
3025 s.complete_at.insert("a".into(), 42);
3028 d.commit(&mut s, &[(partition, 42)]).unwrap();
3029
3030 let committed = script.commits().last().cloned().expect("the commit landed");
3032 assert_eq!(committed.0.as_str(), "a");
3033 assert_eq!(
3034 committed.1.watermark, 42,
3035 "the guard strips the flag, not the commit"
3036 );
3037 assert!(
3038 !committed.1.completed,
3039 "a drain cut is never terminal, whatever the source claims"
3040 );
3041
3042 assert!(
3045 d.assignments().iter().any(|(id, _)| id.as_str() == "a"),
3046 "still owned"
3047 );
3048 assert!(s.closed.is_empty(), "not retired");
3049
3050 s.ready_progress
3052 .borrow_mut()
3053 .insert("a".into(), SplitProgress::new(50, vec![]));
3054 let event = poll(&mut d, &mut s);
3055 let SourceEvent::LanesRetired { lanes } = event else {
3056 panic!("the drained revocation must finally retire, got {event:?}");
3057 };
3058 assert_eq!(lanes, vec![LaneId(0)]);
3059 assert_eq!(script.released_drained(), vec![SplitId::new("a").unwrap()]);
3060 assert!(
3061 !script.commits().last().unwrap().1.completed,
3062 "the final revocation commit is not terminal either"
3063 );
3064 }
3065
3066 #[test]
3067 fn a_completed_progress_during_a_drain_is_never_terminal_in_the_final_commit() {
3068 let script = Script::default();
3069 let mut d = driver(&script);
3070 let mut s = TestSource {
3071 accept_revoke: HashSet::from(["a".to_string()]),
3072 ..TestSource::default()
3073 };
3074 script.push(vec![gained("a", 1, None)]);
3075 poll(&mut d, &mut s);
3076 let partition = s.opened[0].3;
3077 script.push(vec![CoordinationEvent::RevokeRequested {
3078 split: SplitId::new("a").unwrap(),
3079 }]);
3080 assert!(matches!(poll(&mut d, &mut s), SourceEvent::Idle));
3081 assert_eq!(s.begin_revoke_calls, ["a"]);
3082
3083 s.complete_at.insert("a".into(), 42);
3084 let unstored = d.commit_final(&mut s, &[(partition, 42)]).unwrap();
3085
3086 assert!(unstored.is_empty(), "{unstored:?}");
3087 let committed = script.commits().last().cloned().expect("the commit landed");
3088 assert_eq!(committed.0.as_str(), "a");
3089 assert_eq!(committed.1.watermark, 42);
3090 assert!(!committed.1.completed, "a drain cut is never terminal");
3091 assert!(
3092 d.assignments().iter().any(|(id, _)| id.as_str() == "a"),
3093 "still owned"
3094 );
3095 assert!(s.closed.is_empty(), "not retired");
3096 }
3097
3098 #[test]
3099 fn commit_final_settles_each_split_and_returns_the_unstored_partitions() {
3100 let script = Script::default();
3101 let mut d = driver(&script);
3102 let mut s = TestSource::default();
3103 script.push(vec![
3104 gained("a", 1, None),
3105 gained("b", 1, None),
3106 gained("c", 1, None),
3107 gained("d", 1, None),
3108 ]);
3109 poll(&mut d, &mut s);
3110 let [pa, pb, pc, pd] = [0, 1, 2, 3].map(|i| s.opened[i].3);
3111 s.complete_at.insert("b".into(), 20);
3112 script.fail_next_commit("c", CoordinationErrorKind::Retryable);
3113 script.fail_next_commit("d", CoordinationErrorKind::Fenced);
3114
3115 let unstored = d
3116 .commit_final(&mut s, &[(pa, 10), (pb, 20), (pc, 30), (pd, 40)])
3117 .unwrap();
3118
3119 assert_eq!(unstored, vec![pc], "only the deferred split is unstored");
3120 let landed: Vec<(String, i64)> = script
3121 .commits()
3122 .iter()
3123 .map(|(id, p)| (id.as_str().to_string(), p.watermark))
3124 .collect();
3125 assert_eq!(landed, [("a".to_string(), 10), ("b".to_string(), 20)]);
3126 let watermark = |p: PartitionId| d.tenancies[&p].progress.as_ref().map(|p| p.watermark);
3127 assert_eq!(watermark(pa), Some(10), "a durable commit folds");
3128 assert!(d.tenancies[&pb].completed, "a completing commit completes");
3129 assert_eq!(watermark(pc), Some(30), "a deferred commit is cached");
3130 assert!(d.tenancies[&pd].fenced, "a fenced commit fences");
3131 assert_eq!(s.closed, vec!["b", "d"], "completed and fenced retire");
3132 }
3133
3134 #[test]
3135 fn a_fatal_final_answer_is_returned_after_the_later_answers_settle() {
3136 let script = Script::default();
3137 let mut d = driver(&script);
3138 let mut s = TestSource::default();
3139 script.push(vec![gained("a", 1, None), gained("b", 1, None)]);
3140 poll(&mut d, &mut s);
3141 let [pa, pb] = [0, 1].map(|i| s.opened[i].3);
3142 script.fail_next_commit("a", CoordinationErrorKind::Fatal);
3143 script.fail_next_commit("b", CoordinationErrorKind::Fenced);
3144 let result = d.commit_final(&mut s, &[(pa, 10), (pb, 20)]);
3145 assert!(result.is_err(), "the fatal answer is returned");
3146 assert!(
3147 d.tenancies[&pb].fenced,
3148 "the later fenced answer still settles"
3149 );
3150 assert_eq!(s.closed, vec!["b"]);
3151 }
3152
3153 #[test]
3156 fn a_commit_error_names_the_stop_only_when_the_stop_began() {
3157 let script = Script::default();
3158 let mut d = driver(&script);
3159 let mut s = TestSource::default();
3160 let flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
3161 d.set_stop(crate::source::StopSignal::new(Arc::clone(&flag)));
3162 script.push(vec![gained("a", 1, None)]);
3163 poll(&mut d, &mut s);
3164 let pa = s.opened[0].3;
3165
3166 script.fail_next_commit("a", CoordinationErrorKind::Retryable);
3167 let deferred = d.commit(&mut s, &[(pa, 10)]).expect_err("deferred");
3168 assert!(!deferred.to_string().contains("stop"), "{deferred}");
3169
3170 script.stop_on_commit("a", &flag);
3172 script.fail_next_commit("a", CoordinationErrorKind::Retryable);
3173 let cut_short = d.commit(&mut s, &[(pa, 10)]).expect_err("cut short");
3174 assert!(
3175 cut_short.to_string().contains("the stop began"),
3176 "{cut_short}"
3177 );
3178 }
3179}