1use std::{
53 marker::PhantomData,
54 sync::{Arc, Mutex},
55 time::{Duration, Instant},
56};
57
58use async_trait::async_trait;
59use qubit_clock::sleep::{AsyncSleeper, SystemSleeper};
60use tokio::{sync::watch, task::JoinHandle};
61
62use crate::{error::ConnectError, record::SourceOffset, sink::Sink, source::Source};
63
64#[async_trait]
72pub trait CheckpointStore: Send + Sync + 'static {
73 async fn save(&self, offset: &SourceOffset) -> Result<(), ConnectError>;
80
81 async fn load(&self) -> Result<Option<SourceOffset>, ConnectError>;
88}
89
90#[derive(Debug, Default)]
94pub struct InMemoryCheckpointStore {
95 offset: Mutex<Option<SourceOffset>>,
96}
97
98#[async_trait]
99impl CheckpointStore for InMemoryCheckpointStore {
100 async fn save(&self, offset: &SourceOffset) -> Result<(), ConnectError> {
101 *self.offset.lock().expect("checkpoint mutex poisoned") = Some(offset.clone());
102 Ok(())
103 }
104
105 async fn load(&self) -> Result<Option<SourceOffset>, ConnectError> {
106 Ok(self
107 .offset
108 .lock()
109 .expect("checkpoint mutex poisoned")
110 .clone())
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum RuntimeState {
118 Starting,
120 Running,
122 Paused,
125 Draining,
127 Stopped,
129 Failed,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135enum Control {
136 Run,
137 Pause,
138 Shutdown,
139}
140
141#[derive(Debug, Clone, Copy)]
143struct Config {
144 commit_interval: Duration,
145 max_batch: usize,
146 poll_backoff: Duration,
147}
148
149impl Default for Config {
150 fn default() -> Self {
151 Self {
152 commit_interval: Duration::from_secs(5),
153 max_batch: 500,
154 poll_backoff: Duration::from_millis(100),
155 }
156 }
157}
158
159pub struct ConnectorRuntime<K, V, S = NoSource, T = NoSink> {
182 source: S,
183 sink: T,
184 checkpoints: Arc<dyn CheckpointStore>,
185 config: Config,
186 sleeper: Arc<dyn AsyncSleeper>,
190 _marker: PhantomData<(K, V)>,
191}
192
193pub struct NoSource;
196
197pub struct HasSource<K, V>(Box<dyn Source<K, V>>);
199
200pub struct NoSink;
203
204pub struct HasSink<K, V>(Box<dyn Sink<K, V>>);
206
207impl<K, V> Default for ConnectorRuntime<K, V, NoSource, NoSink> {
208 fn default() -> Self {
209 Self {
210 source: NoSource,
211 sink: NoSink,
212 checkpoints: Arc::new(InMemoryCheckpointStore::default()),
213 config: Config::default(),
214 sleeper: Arc::new(SystemSleeper::new()),
215 _marker: PhantomData,
216 }
217 }
218}
219
220impl<K, V> ConnectorRuntime<K, V, NoSource, NoSink>
221where
222 K: Send + 'static,
223 V: Send + 'static,
224{
225 #[must_use]
228 pub fn new() -> Self {
229 Self::default()
230 }
231}
232
233impl<K, V, S, T> ConnectorRuntime<K, V, S, T>
234where
235 K: Send + 'static,
236 V: Send + 'static,
237{
238 #[must_use]
240 pub fn add_source<Src: Source<K, V>>(
241 self,
242 source: Src,
243 ) -> ConnectorRuntime<K, V, HasSource<K, V>, T> {
244 ConnectorRuntime {
245 source: HasSource(Box::new(source)),
246 sink: self.sink,
247 checkpoints: self.checkpoints,
248 config: self.config,
249 sleeper: self.sleeper,
250 _marker: PhantomData,
251 }
252 }
253
254 #[must_use]
256 pub fn add_sink<Snk: Sink<K, V>>(self, sink: Snk) -> ConnectorRuntime<K, V, S, HasSink<K, V>> {
257 ConnectorRuntime {
258 source: self.source,
259 sink: HasSink(Box::new(sink)),
260 checkpoints: self.checkpoints,
261 config: self.config,
262 sleeper: self.sleeper,
263 _marker: PhantomData,
264 }
265 }
266
267 #[must_use]
270 pub fn checkpoint_store(mut self, store: Arc<dyn CheckpointStore>) -> Self {
271 self.checkpoints = store;
272 self
273 }
274
275 #[must_use]
279 pub fn commit_interval(mut self, interval: Duration) -> Self {
280 self.config.commit_interval = interval;
281 self
282 }
283
284 #[must_use]
287 pub fn max_batch(mut self, max_batch: usize) -> Self {
288 self.config.max_batch = max_batch.max(1);
289 self
290 }
291
292 #[must_use]
295 pub fn poll_backoff(mut self, backoff: Duration) -> Self {
296 self.config.poll_backoff = backoff;
297 self
298 }
299
300 #[must_use]
305 pub fn sleeper(mut self, sleeper: Arc<dyn AsyncSleeper>) -> Self {
306 self.sleeper = sleeper;
307 self
308 }
309}
310
311impl<K, V> ConnectorRuntime<K, V, HasSource<K, V>, HasSink<K, V>>
312where
313 K: Send + 'static,
314 V: Send + 'static,
315{
316 #[must_use]
323 pub fn run(self) -> ConnectorHandle {
324 let source = self.source.0;
325 let sink = self.sink.0;
326
327 let (control_tx, control_rx) = watch::channel(Control::Run);
328 let (state_tx, state_rx) = watch::channel(RuntimeState::Starting);
329
330 let driver = Driver {
331 source,
332 sink,
333 checkpoints: self.checkpoints,
334 config: self.config,
335 sleeper: self.sleeper,
336 control: control_rx,
337 state: state_tx,
338 };
339 let join = tokio::spawn(driver.run());
340
341 ConnectorHandle {
342 control: control_tx,
343 state: state_rx,
344 join: Some(join),
345 }
346 }
347}
348
349pub struct ConnectorHandle {
354 control: watch::Sender<Control>,
355 state: watch::Receiver<RuntimeState>,
356 join: Option<JoinHandle<Result<(), ConnectError>>>,
357}
358
359impl ConnectorHandle {
360 pub fn pause(&self) {
363 let _ = self.control.send(Control::Pause);
364 }
365
366 pub fn resume(&self) {
368 let _ = self.control.send(Control::Run);
369 }
370
371 #[must_use]
373 pub fn state(&self) -> RuntimeState {
374 *self.state.borrow()
375 }
376
377 #[tracing::instrument(level = "info", skip_all, err)]
386 pub async fn shutdown(mut self) -> Result<(), ConnectError> {
387 let _ = self.control.send(Control::Shutdown);
388 let join = self.join.take().expect("join handle taken once");
389 match join.await {
390 Ok(result) => result,
391 Err(e) => Err(ConnectError::Backend(format!(
392 "connector task panicked: {e}"
393 ))),
394 }
395 }
396}
397
398impl Drop for ConnectorHandle {
399 fn drop(&mut self) {
400 let _ = self.control.send(Control::Shutdown);
404 }
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409enum Progress {
410 Wrote,
411 CaughtUp,
412}
413
414struct Driver<K, V> {
417 source: Box<dyn Source<K, V>>,
418 sink: Box<dyn Sink<K, V>>,
419 checkpoints: Arc<dyn CheckpointStore>,
420 config: Config,
421 sleeper: Arc<dyn AsyncSleeper>,
422 control: watch::Receiver<Control>,
423 state: watch::Sender<RuntimeState>,
424}
425
426impl<K, V> Driver<K, V>
427where
428 K: Send + 'static,
429 V: Send + 'static,
430{
431 #[tracing::instrument(level = "info", skip_all, err)]
434 async fn run(mut self) -> Result<(), ConnectError> {
435 let result = self.seek_and_loop().await;
436
437 let close = self.close_ends().await;
440 let result = result.and(close);
441
442 let _ = self.state.send(if result.is_ok() {
443 RuntimeState::Stopped
444 } else {
445 RuntimeState::Failed
446 });
447 result
448 }
449
450 #[tracing::instrument(level = "info", skip_all, err)]
451 async fn seek_and_loop(&mut self) -> Result<(), ConnectError> {
452 if let Some(offset) = self.checkpoints.load().await? {
453 tracing::debug!(?offset, "seeking source to restored checkpoint");
454 self.source.seek(offset).await?;
455 }
456 self.main_loop().await
457 }
458
459 async fn main_loop(&mut self) -> Result<(), ConnectError> {
460 loop {
461 let control = *self.control.borrow_and_update();
462 match control {
463 Control::Run => {}
464 Control::Pause => {
465 let _ = self.state.send(RuntimeState::Paused);
466 if self.control.changed().await.is_err() {
469 break;
470 }
471 continue;
472 }
473 Control::Shutdown => break,
474 }
475
476 let _ = self.state.send(RuntimeState::Running);
477 if self.run_once().await? == Progress::CaughtUp {
478 let sleeper = self.sleeper.clone();
484 tokio::select! {
485 () = sleeper.sleep_for_async(self.config.poll_backoff) => {}
486 _ = self.control.changed() => {}
487 }
488 }
489 }
490
491 let _ = self.state.send(RuntimeState::Draining);
497 self.run_once().await?;
498 Ok(())
499 }
500
501 #[tracing::instrument(
504 level = "debug",
505 skip_all,
506 fields(batch = tracing::field::Empty, caught_up = tracing::field::Empty),
507 err,
508 )]
509 async fn run_once(&mut self) -> Result<Progress, ConnectError> {
510 let deadline = Instant::now() + self.config.commit_interval;
511 let mut batch = Vec::new();
512 let mut caught_up = false;
513
514 while batch.len() < self.config.max_batch {
515 if let Some(record) = self.source.poll().await? {
516 batch.push(record);
517 } else {
518 caught_up = true;
519 break;
520 }
521 if Instant::now() >= deadline {
522 break;
523 }
524 }
525
526 let span = tracing::Span::current();
527 span.record("batch", batch.len());
528 span.record("caught_up", caught_up);
529
530 if !batch.is_empty() {
531 self.write_committed(batch).await?;
532 }
533
534 Ok(if caught_up {
535 Progress::CaughtUp
536 } else {
537 Progress::Wrote
538 })
539 }
540
541 #[tracing::instrument(
545 level = "debug",
546 skip_all,
547 fields(records = batch.len(), transactional = tracing::field::Empty),
548 err,
549 )]
550 async fn write_committed(
551 &mut self,
552 batch: Vec<crate::record::ConnectRecord<K, V>>,
553 ) -> Result<(), ConnectError> {
554 let count = batch.len();
555 let transactional = self.sink.supports_transactions();
556 tracing::Span::current().record("transactional", transactional);
557
558 if transactional {
561 self.sink.begin().await?;
562 }
563
564 if let Err(e) = self.deliver(batch).await {
565 if transactional {
566 if let Err(abort_err) = self.sink.abort().await {
569 tracing::warn!(error = %abort_err, "sink abort failed after delivery error");
570 }
571 }
572 return Err(e);
573 }
574
575 if let Some(offset) = self.source.checkpoint() {
578 self.checkpoints.save(&offset).await?;
579 self.source.acknowledge(&offset).await?;
580 }
581 tracing::debug!(records = count, transactional, "committed connector batch");
582 Ok(())
583 }
584
585 async fn deliver(
588 &mut self,
589 batch: Vec<crate::record::ConnectRecord<K, V>>,
590 ) -> Result<(), ConnectError> {
591 self.sink.put(batch).await?;
592 self.sink.commit().await
593 }
594
595 #[tracing::instrument(level = "info", skip_all, err)]
596 async fn close_ends(&mut self) -> Result<(), ConnectError> {
597 let source_close = self.source.close().await;
598 let sink_close = self.sink.close().await;
599 source_close.and(sink_close)
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use std::sync::atomic::{AtomicUsize, Ordering};
606
607 use assert2::check;
608 use async_trait::async_trait;
609 use bytes::Bytes;
610 use qubit_clock::{MockTimeline, sleep::MockSleeper};
611 use tokio::sync::mpsc;
612
613 use super::*;
614 use crate::record::{ConnectRecord, OffsetMap, OffsetValue};
615
616 async fn await_until(what: &str, mut cond: impl FnMut() -> bool) {
620 for _ in 0..100_000 {
621 if cond() {
622 return;
623 }
624 tokio::task::yield_now().await;
625 }
626 panic!("condition never held: {what}");
627 }
628
629 struct VecSource {
632 records: Vec<Bytes>,
633 pos: usize,
634 }
635
636 impl VecSource {
637 fn new(values: &[&'static [u8]]) -> Self {
638 Self {
639 records: values.iter().map(|v| Bytes::from_static(v)).collect(),
640 pos: 0,
641 }
642 }
643 }
644
645 #[async_trait]
646 impl Source<Bytes, Bytes> for VecSource {
647 async fn poll(&mut self) -> Result<Option<ConnectRecord<Bytes, Bytes>>, ConnectError> {
648 let Some(v) = self.records.get(self.pos).cloned() else {
649 return Ok(None);
650 };
651 self.pos += 1;
652 Ok(Some(ConnectRecord::new(None, Some(v))))
653 }
654
655 fn checkpoint(&self) -> Option<SourceOffset> {
656 if self.pos == 0 {
657 return None;
658 }
659 let mut position = OffsetMap::new();
660 #[allow(clippy::cast_possible_wrap)]
661 position.insert("index".into(), OffsetValue::Long(self.pos as i64));
662 Some(SourceOffset::new(OffsetMap::new().into(), position.into()))
663 }
664
665 async fn seek(&mut self, offset: SourceOffset) -> Result<(), ConnectError> {
666 match offset.position.get("index") {
667 Some(OffsetValue::Long(i)) => {
668 self.pos = usize::try_from(*i).unwrap();
669 Ok(())
670 }
671 _ => Err(ConnectError::Offset("missing index".into())),
672 }
673 }
674 }
675
676 struct CountingIdleSource {
679 polls: Arc<AtomicUsize>,
680 }
681
682 #[async_trait]
683 impl Source<Bytes, Bytes> for CountingIdleSource {
684 async fn poll(&mut self) -> Result<Option<ConnectRecord<Bytes, Bytes>>, ConnectError> {
685 self.polls.fetch_add(1, Ordering::SeqCst);
686 Ok(None)
687 }
688 fn checkpoint(&self) -> Option<SourceOffset> {
689 None
690 }
691 async fn seek(&mut self, _offset: SourceOffset) -> Result<(), ConnectError> {
692 Ok(())
693 }
694 }
695
696 struct ChannelSink {
700 tx: mpsc::UnboundedSender<Bytes>,
701 transactional: bool,
702 begins: Arc<AtomicUsize>,
703 commits: Arc<AtomicUsize>,
704 puts: Arc<Mutex<Vec<usize>>>,
705 staged: Vec<Bytes>,
706 }
707
708 #[async_trait]
709 impl Sink<Bytes, Bytes> for ChannelSink {
710 async fn put(
711 &mut self,
712 records: Vec<ConnectRecord<Bytes, Bytes>>,
713 ) -> Result<(), ConnectError> {
714 self.puts.lock().unwrap().push(records.len());
715 self.staged
716 .extend(records.into_iter().filter_map(|r| r.value));
717 Ok(())
718 }
719
720 async fn flush(&mut self) -> Result<(), ConnectError> {
721 self.commits.fetch_add(1, Ordering::SeqCst);
722 for v in self.staged.drain(..) {
723 let _ = self.tx.send(v);
724 }
725 Ok(())
726 }
727
728 fn supports_transactions(&self) -> bool {
729 self.transactional
730 }
731
732 async fn begin(&mut self) -> Result<(), ConnectError> {
733 self.begins.fetch_add(1, Ordering::SeqCst);
734 Ok(())
735 }
736
737 async fn abort(&mut self) -> Result<(), ConnectError> {
738 self.staged.clear();
739 Ok(())
740 }
741 }
742
743 fn channel_sink(transactional: bool) -> (ChannelSink, mpsc::UnboundedReceiver<Bytes>) {
744 let (tx, rx) = mpsc::unbounded_channel();
745 (
746 ChannelSink {
747 tx,
748 transactional,
749 begins: Arc::new(AtomicUsize::new(0)),
750 commits: Arc::new(AtomicUsize::new(0)),
751 puts: Arc::new(Mutex::new(Vec::new())),
752 staged: Vec::new(),
753 },
754 rx,
755 )
756 }
757
758 async fn collect(rx: &mut mpsc::UnboundedReceiver<Bytes>, n: usize) -> Vec<Bytes> {
763 let mut out = Vec::new();
764 for _ in 0..n {
765 let rec = tokio::time::timeout(Duration::from_secs(5), rx.recv())
766 .await
767 .expect("record delivered within 5s")
768 .expect("sink channel stayed open");
769 out.push(rec);
770 }
771 out
772 }
773
774 async fn shutdown(handle: ConnectorHandle) -> Result<(), ConnectError> {
777 tokio::time::timeout(Duration::from_secs(10), handle.shutdown())
778 .await
779 .expect("runtime shut down within 10s")
780 }
781
782 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
783 async fn pipes_source_records_to_sink_in_order() {
784 let (sink, mut rx) = channel_sink(false);
785 let handle = ConnectorRuntime::new()
786 .add_source(VecSource::new(&[b"a", b"b", b"c"]))
787 .add_sink(sink)
788 .poll_backoff(Duration::from_millis(5))
789 .run();
790
791 let got = collect(&mut rx, 3).await;
792 check!(
793 got == vec![
794 Bytes::from_static(b"a"),
795 Bytes::from_static(b"b"),
796 Bytes::from_static(b"c")
797 ]
798 );
799 shutdown(handle).await.unwrap();
800 }
801
802 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
803 async fn transactional_sink_brackets_each_nonempty_commit() {
804 let (sink, mut rx) = channel_sink(true);
805 let begins = sink.begins.clone();
806 let commits = sink.commits.clone();
807 let handle = ConnectorRuntime::new()
808 .add_source(VecSource::new(&[b"x"]))
809 .add_sink(sink)
810 .poll_backoff(Duration::from_millis(5))
811 .run();
812
813 let got = collect(&mut rx, 1).await;
814 check!(got == vec![Bytes::from_static(b"x")]);
815 shutdown(handle).await.unwrap();
816
817 check!(begins.load(Ordering::SeqCst) == 1);
820 check!(commits.load(Ordering::SeqCst) == 1);
821 }
822
823 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
824 async fn checkpoint_persists_and_restart_resumes_after_it() {
825 let store = Arc::new(InMemoryCheckpointStore::default());
826
827 let (sink, mut rx) = channel_sink(false);
828 let handle = ConnectorRuntime::new()
829 .add_source(VecSource::new(&[b"a", b"b"]))
830 .add_sink(sink)
831 .checkpoint_store(store.clone())
832 .poll_backoff(Duration::from_millis(5))
833 .run();
834 let _ = collect(&mut rx, 2).await;
835 shutdown(handle).await.unwrap();
836
837 let saved = store.load().await.unwrap().expect("checkpoint saved");
839 check!(saved.position.get("index") == Some(&OffsetValue::Long(2)));
840
841 let (sink2, mut rx2) = channel_sink(false);
844 let handle2 = ConnectorRuntime::new()
845 .add_source(VecSource::new(&[b"a", b"b"]))
846 .add_sink(sink2)
847 .checkpoint_store(store.clone())
848 .poll_backoff(Duration::from_millis(5))
849 .run();
850 tokio::time::sleep(Duration::from_millis(50)).await;
852 shutdown(handle2).await.unwrap();
853 check!(rx2.try_recv().is_err());
854 }
855
856 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
857 async fn pause_stops_polling_and_resume_restarts_it() {
858 let polls = Arc::new(AtomicUsize::new(0));
859 let (sink, _rx) = channel_sink(false);
860 let handle = ConnectorRuntime::new()
861 .add_source(CountingIdleSource {
862 polls: polls.clone(),
863 })
864 .add_sink(sink)
865 .poll_backoff(Duration::from_millis(5))
866 .run();
867
868 tokio::time::sleep(Duration::from_millis(40)).await;
870 handle.pause();
871 tokio::time::sleep(Duration::from_millis(20)).await;
872 check!(handle.state() == RuntimeState::Paused);
873
874 let paused_count = polls.load(Ordering::SeqCst);
875 tokio::time::sleep(Duration::from_millis(40)).await;
876 check!(polls.load(Ordering::SeqCst) == paused_count);
878
879 handle.resume();
880 tokio::time::sleep(Duration::from_millis(40)).await;
881 check!(polls.load(Ordering::SeqCst) > paused_count);
883 shutdown(handle).await.unwrap();
884 }
885
886 struct FailingSink {
888 aborts: Arc<AtomicUsize>,
889 }
890
891 #[async_trait]
892 impl Sink<Bytes, Bytes> for FailingSink {
893 async fn put(
894 &mut self,
895 _records: Vec<ConnectRecord<Bytes, Bytes>>,
896 ) -> Result<(), ConnectError> {
897 Err(ConnectError::Backend("write rejected".into()))
898 }
899 async fn flush(&mut self) -> Result<(), ConnectError> {
900 Ok(())
901 }
902 fn supports_transactions(&self) -> bool {
903 true
904 }
905 async fn abort(&mut self) -> Result<(), ConnectError> {
906 self.aborts.fetch_add(1, Ordering::SeqCst);
907 Ok(())
908 }
909 }
910
911 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
912 async fn delivery_failure_aborts_transaction_and_fails_runtime() {
913 let aborts = Arc::new(AtomicUsize::new(0));
914 let handle = ConnectorRuntime::new()
915 .add_source(VecSource::new(&[b"a"]))
916 .add_sink(FailingSink {
917 aborts: aborts.clone(),
918 })
919 .poll_backoff(Duration::from_millis(5))
920 .run();
921
922 let result = shutdown(handle).await;
924 check!(result.is_err());
925 check!(aborts.load(Ordering::SeqCst) == 1);
926 }
927
928 struct SeekFailSource;
931
932 #[async_trait]
933 impl Source<Bytes, Bytes> for SeekFailSource {
934 async fn poll(&mut self) -> Result<Option<ConnectRecord<Bytes, Bytes>>, ConnectError> {
935 Ok(None)
936 }
937 fn checkpoint(&self) -> Option<SourceOffset> {
938 None
939 }
940 async fn seek(&mut self, _offset: SourceOffset) -> Result<(), ConnectError> {
941 Err(ConnectError::Offset(
942 "upstream truncated past offset".into(),
943 ))
944 }
945 }
946
947 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
948 async fn restored_checkpoint_seek_failure_fails_runtime() {
949 let store = Arc::new(InMemoryCheckpointStore::default());
951 store.save(&SourceOffset::default()).await.unwrap();
952
953 let handle = ConnectorRuntime::new()
954 .add_source(SeekFailSource)
955 .add_sink(channel_sink(false).0)
956 .checkpoint_store(store)
957 .run();
958 check!(shutdown(handle).await.is_err());
959 }
960
961 struct FailingCheckpointStore {
964 fail_load: bool,
965 }
966
967 #[async_trait]
968 impl CheckpointStore for FailingCheckpointStore {
969 async fn save(&self, _offset: &SourceOffset) -> Result<(), ConnectError> {
970 if self.fail_load {
971 Ok(())
972 } else {
973 Err(ConnectError::Backend("save rejected".into()))
974 }
975 }
976 async fn load(&self) -> Result<Option<SourceOffset>, ConnectError> {
977 if self.fail_load {
978 Err(ConnectError::Backend("load rejected".into()))
979 } else {
980 Ok(None)
981 }
982 }
983 }
984
985 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
986 async fn checkpoint_load_error_fails_runtime_at_startup() {
987 let handle = ConnectorRuntime::new()
988 .add_source(VecSource::new(&[b"a"]))
989 .add_sink(channel_sink(false).0)
990 .checkpoint_store(Arc::new(FailingCheckpointStore { fail_load: true }))
991 .run();
992 check!(shutdown(handle).await.is_err());
993 }
994
995 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
996 async fn checkpoint_save_error_after_commit_fails_runtime() {
997 let (sink, mut rx) = channel_sink(false);
1000 let handle = ConnectorRuntime::new()
1001 .add_source(VecSource::new(&[b"a"]))
1002 .add_sink(sink)
1003 .checkpoint_store(Arc::new(FailingCheckpointStore { fail_load: false }))
1004 .poll_backoff(Duration::from_millis(5))
1005 .run();
1006 check!(collect(&mut rx, 1).await == vec![Bytes::from_static(b"a")]);
1007 check!(shutdown(handle).await.is_err());
1008 }
1009
1010 struct OrderingSource {
1011 emitted: bool,
1012 events: Arc<Mutex<Vec<&'static str>>>,
1013 }
1014
1015 #[async_trait]
1016 impl Source<Bytes, Bytes> for OrderingSource {
1017 async fn poll(&mut self) -> Result<Option<ConnectRecord<Bytes, Bytes>>, ConnectError> {
1018 if self.emitted {
1019 return Ok(None);
1020 }
1021 self.emitted = true;
1022 Ok(Some(ConnectRecord::new(
1023 None,
1024 Some(Bytes::from_static(b"a")),
1025 )))
1026 }
1027
1028 fn checkpoint(&self) -> Option<SourceOffset> {
1029 self.emitted.then(|| {
1030 let mut position = OffsetMap::new();
1031 position.insert("index".into(), OffsetValue::Long(1));
1032 SourceOffset::new(OffsetMap::new().into(), position.into())
1033 })
1034 }
1035
1036 async fn seek(&mut self, _offset: SourceOffset) -> Result<(), ConnectError> {
1037 Ok(())
1038 }
1039
1040 async fn acknowledge(&mut self, _offset: &SourceOffset) -> Result<(), ConnectError> {
1041 self.events.lock().unwrap().push("acknowledge");
1042 Ok(())
1043 }
1044 }
1045
1046 struct OrderingSink {
1047 events: Arc<Mutex<Vec<&'static str>>>,
1048 }
1049
1050 #[async_trait]
1051 impl Sink<Bytes, Bytes> for OrderingSink {
1052 async fn put(
1053 &mut self,
1054 _records: Vec<ConnectRecord<Bytes, Bytes>>,
1055 ) -> Result<(), ConnectError> {
1056 self.events.lock().unwrap().push("put");
1057 Ok(())
1058 }
1059
1060 async fn flush(&mut self) -> Result<(), ConnectError> {
1061 self.events.lock().unwrap().push("commit");
1062 Ok(())
1063 }
1064 }
1065
1066 struct OrderingCheckpointStore {
1067 events: Arc<Mutex<Vec<&'static str>>>,
1068 saved: Mutex<Option<SourceOffset>>,
1069 }
1070
1071 #[async_trait]
1072 impl CheckpointStore for OrderingCheckpointStore {
1073 async fn save(&self, offset: &SourceOffset) -> Result<(), ConnectError> {
1074 self.events.lock().unwrap().push("checkpoint_save");
1075 *self.saved.lock().unwrap() = Some(offset.clone());
1076 Ok(())
1077 }
1078
1079 async fn load(&self) -> Result<Option<SourceOffset>, ConnectError> {
1080 Ok(self.saved.lock().unwrap().clone())
1081 }
1082 }
1083
1084 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1085 async fn source_acknowledge_runs_after_sink_commit_and_checkpoint_save() {
1086 let events = Arc::new(Mutex::new(Vec::new()));
1087 let store = Arc::new(OrderingCheckpointStore {
1088 events: events.clone(),
1089 saved: Mutex::new(None),
1090 });
1091 let handle = ConnectorRuntime::new()
1092 .add_source(OrderingSource {
1093 emitted: false,
1094 events: events.clone(),
1095 })
1096 .add_sink(OrderingSink {
1097 events: events.clone(),
1098 })
1099 .checkpoint_store(store)
1100 .poll_backoff(Duration::from_millis(5))
1101 .run();
1102
1103 tokio::time::sleep(Duration::from_millis(50)).await;
1104 shutdown(handle).await.unwrap();
1105
1106 check!(
1107 events.lock().unwrap().as_slice()
1108 == ["put", "commit", "checkpoint_save", "acknowledge"]
1109 );
1110 }
1111
1112 struct CloseFailSink;
1115
1116 #[async_trait]
1117 impl Sink<Bytes, Bytes> for CloseFailSink {
1118 async fn put(
1119 &mut self,
1120 _records: Vec<ConnectRecord<Bytes, Bytes>>,
1121 ) -> Result<(), ConnectError> {
1122 Ok(())
1123 }
1124 async fn flush(&mut self) -> Result<(), ConnectError> {
1125 Ok(())
1126 }
1127 async fn close(&mut self) -> Result<(), ConnectError> {
1128 Err(ConnectError::Backend("close rejected".into()))
1129 }
1130 }
1131
1132 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1133 async fn close_failure_surfaces_after_clean_run() {
1134 let handle = ConnectorRuntime::new()
1135 .add_source(VecSource::new(&[b"a"]))
1136 .add_sink(CloseFailSink)
1137 .poll_backoff(Duration::from_millis(5))
1138 .run();
1139 check!(shutdown(handle).await.is_err());
1141 }
1142
1143 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1144 async fn one_caught_up_interval_batches_all_available_records() {
1145 let (sink, mut rx) = channel_sink(false);
1150 let puts = sink.puts.clone();
1151 let handle = ConnectorRuntime::new()
1152 .add_source(VecSource::new(&[b"a", b"b", b"c"]))
1153 .add_sink(sink)
1154 .max_batch(100)
1155 .commit_interval(Duration::from_secs(30))
1156 .poll_backoff(Duration::from_millis(5))
1157 .run();
1158 let _ = collect(&mut rx, 3).await;
1159 shutdown(handle).await.unwrap();
1160 check!(*puts.lock().unwrap() == vec![3]);
1161 }
1162
1163 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1164 async fn max_batch_caps_each_put() {
1165 let (sink, mut rx) = channel_sink(false);
1168 let puts = sink.puts.clone();
1169 let handle = ConnectorRuntime::new()
1170 .add_source(VecSource::new(&[b"a", b"b", b"c", b"d"]))
1171 .add_sink(sink)
1172 .max_batch(2)
1173 .commit_interval(Duration::from_secs(30))
1174 .poll_backoff(Duration::from_millis(5))
1175 .run();
1176 let _ = collect(&mut rx, 4).await;
1177 shutdown(handle).await.unwrap();
1178 let sizes = puts.lock().unwrap().clone();
1179 check!(sizes.iter().all(|&n| n <= 2));
1180 check!(sizes.iter().sum::<usize>() == 4);
1181 }
1182
1183 #[tokio::test]
1184 async fn idle_source_backs_off_between_polls() {
1185 const ADVANCES: usize = 5;
1191 let polls = Arc::new(AtomicUsize::new(0));
1192 let (sink, _rx) = channel_sink(false);
1193 let backoff = Duration::from_millis(50);
1194 let sleeper = MockSleeper::new();
1195 let timeline: MockTimeline = sleeper.timeline();
1196 let handle = ConnectorRuntime::new()
1197 .add_source(CountingIdleSource {
1198 polls: polls.clone(),
1199 })
1200 .add_sink(sink)
1201 .poll_backoff(backoff)
1202 .sleeper(Arc::new(sleeper))
1203 .run();
1204
1205 await_until("first idle poll", || polls.load(Ordering::SeqCst) >= 1).await;
1208
1209 for i in 0..ADVANCES {
1212 timeline.advance(backoff);
1213 let expected = i + 2;
1214 await_until("poll after backoff advance", || {
1215 polls.load(Ordering::SeqCst) >= expected
1216 })
1217 .await;
1218 }
1219
1220 check!(polls.load(Ordering::SeqCst) == ADVANCES + 1);
1224 shutdown(handle).await.unwrap();
1225 }
1226
1227 struct ClosingSource {
1230 records: Vec<Bytes>,
1231 pos: usize,
1232 closed: mpsc::UnboundedSender<()>,
1233 }
1234
1235 #[async_trait]
1236 impl Source<Bytes, Bytes> for ClosingSource {
1237 async fn poll(&mut self) -> Result<Option<ConnectRecord<Bytes, Bytes>>, ConnectError> {
1238 let Some(v) = self.records.get(self.pos).cloned() else {
1239 return Ok(None);
1240 };
1241 self.pos += 1;
1242 Ok(Some(ConnectRecord::new(None, Some(v))))
1243 }
1244 fn checkpoint(&self) -> Option<SourceOffset> {
1245 None
1246 }
1247 async fn seek(&mut self, _offset: SourceOffset) -> Result<(), ConnectError> {
1248 Ok(())
1249 }
1250 async fn close(&mut self) -> Result<(), ConnectError> {
1251 let _ = self.closed.send(());
1252 Ok(())
1253 }
1254 }
1255
1256 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1257 async fn dropping_handle_drains_and_closes_the_source() {
1258 let (closed_tx, mut closed_rx) = mpsc::unbounded_channel();
1261 let (sink, _rx) = channel_sink(false);
1262 let handle = ConnectorRuntime::new()
1263 .add_source(ClosingSource {
1264 records: vec![Bytes::from_static(b"a")],
1265 pos: 0,
1266 closed: closed_tx,
1267 })
1268 .add_sink(sink)
1269 .poll_backoff(Duration::from_millis(5))
1270 .run();
1271
1272 drop(handle);
1273 tokio::time::timeout(Duration::from_secs(5), closed_rx.recv())
1274 .await
1275 .expect("source closed within 5s of dropping the handle")
1276 .expect("close signal received");
1277 }
1278}