1use arrow_schema::SchemaRef;
5use async_trait::async_trait;
6use laminar_core::checkpoint::CommittedSourceHandoff;
7use rdkafka::consumer::{CommitMode, Consumer, StreamConsumer};
8use rdkafka::error::KafkaError;
9use rdkafka::message::Message;
10use rdkafka::ClientConfig;
11use rdkafka::TopicPartitionList;
12use std::collections::BTreeSet;
13use std::num::NonZeroU64;
14use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, AtomicUsize, Ordering};
15use std::sync::{Arc, Mutex};
16use tokio::sync::{Mutex as AsyncMutex, Notify};
17use tracing::{debug, info, warn};
18
19use crate::checkpoint::SourceCheckpoint;
20use crate::config::{ConnectorConfig, ConnectorState};
21use crate::connector::{
22 ConnectorTaskGuard, ConnectorTaskOwner, ConnectorTaskTracker, DeliveryGuarantee, SourceBatch,
23 SourceConnector, SourceConsistency, SourceContract, SourceDrainOutcome, SourceDrainRequest,
24 SourceDrainResolution, SourcePosition, SourceStart, SourceTopology,
25};
26use crate::error::{ConnectorError, SerdeError};
27use crate::serde::{self, Format, RecordDeserializer};
28
29use super::avro::AvroDeserializer;
30use super::config::{
31 resolve_value_subject, KafkaSourceConfig, OffsetReset, SchemaEvolutionStrategy, StartupMode,
32 TopicSubscription,
33};
34use super::metadata_error::{fetch_error, invalid_response, topic_error};
35use super::metrics::KafkaSourceMetrics;
36use super::offsets::{OffsetTracker, KAFKA_PARTITION_BASELINE_PREFIX};
37use super::rebalance::RebalanceState;
38use super::schema_registry::SchemaRegistryClient;
39
40use super::rebalance::LaminarConsumerContext;
41use crate::schema::evolution::SchemaEvolution;
42use crate::schema::traits::{CompatibilityMode, EvolutionVerdict};
43
44fn lock_or_recover<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
50 mutex.lock().unwrap_or_else(|poisoned| {
51 tracing::warn!("mutex poisoned, recovering");
52 poisoned.into_inner()
53 })
54}
55
56fn publish_reader_fault(
57 fault: &Mutex<Option<Arc<str>>>,
58 data_ready: &Notify,
59 reason: impl Into<Arc<str>>,
60) {
61 let mut published = lock_or_recover(fault);
62 if published.is_none() {
63 *published = Some(reason.into());
64 }
65 drop(published);
66 data_ready.notify_one();
67}
68
69fn terminalize_guaranteed_poll_error(
74 delivery: DeliveryGuarantee,
75 state: &mut ConnectorState,
76 metrics: &KafkaSourceMetrics,
77 reader_shutdown: Option<&tokio::sync::watch::Sender<bool>>,
78 error: ConnectorError,
79) -> ConnectorError {
80 if delivery == DeliveryGuarantee::BestEffort {
81 return error;
82 }
83
84 metrics.record_error();
85 *state = ConnectorState::Failed;
86 if let Some(shutdown) = reader_shutdown {
87 let _ = shutdown.send(true);
88 }
89 warn!(delivery = %delivery, %error, "Kafka source generation stopped after a post-drain failure");
90
91 if error.is_transient() {
92 ConnectorError::Internal(format!(
93 "Kafka guaranteed-delivery poll failed after draining input; recovery from the durable cursor is required: {error}"
94 ))
95 } else {
96 error
97 }
98}
99
100struct KafkaPayload {
102 data: Vec<u8>,
103 topic: Arc<str>,
104 partition: i32,
105 partition_vnode: Option<u32>,
107 offset: i64,
108 timestamp_ms: Option<i64>,
109 headers_json: Option<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114struct KafkaDrainPartition {
115 topic: Arc<str>,
116 partition: i32,
117}
118
119#[derive(Debug, Clone)]
120struct KafkaDrainBoundary {
121 round: laminar_core::checkpoint::AssignmentDrainId,
122 inputs: Arc<[KafkaDrainPartition]>,
123}
124
125enum KafkaReaderItem {
126 Payload(KafkaPayload),
127 DrainBoundary(KafkaDrainBoundary),
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131struct KafkaDrainPosition {
132 topic: Arc<str>,
133 partition: i32,
134 next_offset: i64,
135}
136
137enum KafkaReaderDrainCommand {
138 Begin {
139 request: SourceDrainRequest,
140 deadline: tokio::time::Instant,
141 },
142 Resolve {
143 resolution: SourceDrainResolution,
144 cut: Arc<[KafkaDrainPosition]>,
145 deadline: tokio::time::Instant,
146 execution: Arc<AtomicU8>,
147 reply: tokio::sync::oneshot::Sender<Result<(), String>>,
148 },
149}
150
151struct KafkaReaderDrain {
152 request: SourceDrainRequest,
153 prepare_deadline: tokio::time::Instant,
154 inputs: Arc<[KafkaDrainPartition]>,
155 held_inputs: Arc<[KafkaDrainPartition]>,
156 held_assignment_version: Option<u64>,
157 hold_complete: bool,
158 boundary_queued: bool,
159}
160
161struct KafkaPendingDrainResolution {
162 resolution: SourceDrainResolution,
163 deadline: tokio::time::Instant,
164 execution: Arc<AtomicU8>,
165 reply: tokio::sync::oneshot::Receiver<Result<(), String>>,
166 terminal_error: Option<Arc<str>>,
167}
168
169struct KafkaSourceDrain {
170 request: SourceDrainRequest,
171 prepare_deadline: tokio::time::Instant,
172 boundary: Option<KafkaDrainBoundary>,
173 cut: Option<Arc<[KafkaDrainPosition]>>,
174 pending_resolution: Option<KafkaPendingDrainResolution>,
175}
176
177const KAFKA_BACKGROUND_CLOSE_BUDGET: std::time::Duration = std::time::Duration::from_millis(500);
178const KAFKA_DRAIN_EXECUTION_PENDING: u8 = 0;
179const KAFKA_DRAIN_EXECUTION_STARTED: u8 = 1;
180const KAFKA_DRAIN_EXECUTION_CANCELLED: u8 = 2;
181const KAFKA_PARTITION_INVENTORY_METADATA: &str = "kafka.partition.inventory.v1";
182const KAFKA_POSITION_LOOKUP_BUDGET: std::time::Duration = std::time::Duration::from_secs(10);
183const KAFKA_POSITION_LOOKUP_CONCURRENCY: usize = 32;
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186enum KafkaBlockingTaskError {
187 Retired,
188 WorkerDropped,
189}
190
191impl std::fmt::Display for KafkaBlockingTaskError {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 match self {
194 Self::Retired => f.write_str("Kafka connector generation retired"),
195 Self::WorkerDropped => f.write_str("Kafka blocking worker ended without a result"),
196 }
197 }
198}
199
200#[derive(Clone)]
207struct KafkaBlockingTasks {
208 retired: Arc<AtomicBool>,
209 handles: Arc<AsyncMutex<Vec<tokio::task::JoinHandle<()>>>>,
210 reaper_started: Arc<AtomicBool>,
211 terminal_guard: Arc<ConnectorTaskGuard>,
212}
213
214impl KafkaBlockingTasks {
215 fn new(terminal_guard: ConnectorTaskGuard) -> Self {
216 Self {
217 retired: Arc::new(AtomicBool::new(false)),
218 handles: Arc::new(AsyncMutex::new(Vec::new())),
219 reaper_started: Arc::new(AtomicBool::new(false)),
220 terminal_guard: Arc::new(terminal_guard),
221 }
222 }
223
224 async fn run<T, F>(&self, operation: F) -> Result<T, KafkaBlockingTaskError>
225 where
226 T: Send + 'static,
227 F: FnOnce() -> T + Send + 'static,
228 {
229 if self.retired.load(Ordering::Acquire) {
230 return Err(KafkaBlockingTaskError::Retired);
231 }
232
233 let (result_tx, result_rx) = tokio::sync::oneshot::channel();
234 let mut handles = self.handles.lock().await;
235 if self.retired.load(Ordering::Acquire) {
236 return Err(KafkaBlockingTaskError::Retired);
237 }
238 let retired = Arc::clone(&self.retired);
241 let terminal_guard = Arc::clone(&self.terminal_guard);
242 handles.push(tokio::task::spawn_blocking(move || {
243 let _terminal_guard = terminal_guard;
244 if retired.load(Ordering::Acquire) {
245 let _ = result_tx.send(Err(KafkaBlockingTaskError::Retired));
246 return;
247 }
248 let result = operation();
249 if retired.load(Ordering::Acquire) {
250 let _ = result_tx.send(Err(KafkaBlockingTaskError::Retired));
251 } else {
252 let _ = result_tx.send(Ok(result));
253 }
254 }));
255 drop(handles);
256
257 let result = result_rx
258 .await
259 .map_err(|_| KafkaBlockingTaskError::WorkerDropped)?;
260 self.reap_finished().await;
261 result
262 }
263
264 async fn reap_finished(&self) {
265 let completed = {
266 let mut handles = self.handles.lock().await;
267 let mut completed = Vec::new();
268 let mut index = 0;
269 while index < handles.len() {
270 if handles[index].is_finished() {
271 completed.push(handles.swap_remove(index));
272 } else {
273 index += 1;
274 }
275 }
276 completed
277 };
278 for handle in completed {
279 if let Err(error) = handle.await {
280 warn!(%error, "Kafka blocking worker failed");
281 }
282 }
283 }
284
285 fn retire(&self) {
286 self.retired.store(true, Ordering::Release);
287 }
288
289 async fn join_until(&self, deadline: tokio::time::Instant) -> bool {
290 self.retire();
291 let mut handles = self.handles.lock().await;
292 for handle in handles.iter() {
293 handle.abort();
294 }
295 while let Some(handle) = handles.first_mut() {
296 match tokio::time::timeout_at(deadline, handle).await {
297 Ok(result) => {
298 if let Err(error) = result {
299 debug!(%error, "Kafka blocking worker cancelled during retirement");
300 }
301 handles.swap_remove(0);
302 }
303 Err(_) => return false,
304 }
305 }
306 self.reaper_started.store(true, Ordering::Release);
307 true
308 }
309
310 fn ensure_reaper(&self) {
311 self.retire();
312 if self.reaper_started.swap(true, Ordering::AcqRel) {
313 return;
314 }
315 if self
319 .handles
320 .try_lock()
321 .is_ok_and(|handles| handles.is_empty())
322 {
323 return;
324 }
325 let generation = self.clone();
326 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
327 self.reaper_started.store(false, Ordering::Release);
328 warn!("Kafka blocking generation retired outside a Tokio runtime");
329 return;
330 };
331 drop(runtime.spawn(async move {
332 let mut handles = generation.handles.lock().await;
333 for handle in handles.iter() {
334 handle.abort();
335 }
336 while let Some(handle) = handles.first_mut() {
337 if let Err(error) = handle.await {
338 debug!(%error, "Kafka blocking worker cancelled during reaping");
339 }
340 handles.swap_remove(0);
341 }
342 }));
343 }
344
345 fn spawn_final_drop<T: Send + Sync + 'static>(&self, owner: Arc<T>, resource: &'static str) {
348 let terminal_guard = Arc::clone(&self.terminal_guard);
349 let reap = move || {
350 let _terminal_guard = terminal_guard;
351 while Arc::strong_count(&owner) > 1 {
352 std::thread::sleep(std::time::Duration::from_millis(1));
353 }
354 drop(owner);
355 };
356 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
357 drop(runtime.spawn_blocking(reap));
358 } else {
359 warn!(resource, "Kafka resource retired outside a Tokio runtime");
360 if let Err(error) = std::thread::Builder::new()
361 .name("laminardb-kafka-resource-drop".into())
362 .spawn(reap)
363 {
364 tracing::error!(resource, %error, "failed to start Kafka resource teardown thread");
365 }
366 }
367 }
368
369 #[cfg(test)]
370 async fn tracked_count(&self) -> usize {
371 self.handles.lock().await.len()
372 }
373}
374
375struct KafkaDrainWaitGuard {
376 execution: Arc<AtomicU8>,
377 armed: bool,
378}
379
380impl KafkaDrainWaitGuard {
381 fn new(execution: Arc<AtomicU8>) -> Self {
382 Self {
383 execution,
384 armed: true,
385 }
386 }
387
388 fn disarm(&mut self) {
389 self.armed = false;
390 }
391}
392
393impl Drop for KafkaDrainWaitGuard {
394 fn drop(&mut self) {
395 if self.armed {
396 let _ = self.execution.compare_exchange(
397 KAFKA_DRAIN_EXECUTION_PENDING,
398 KAFKA_DRAIN_EXECUTION_CANCELLED,
399 Ordering::AcqRel,
400 Ordering::Acquire,
401 );
402 }
403 }
404}
405
406fn claim_kafka_drain_execution(
407 execution: &AtomicU8,
408 deadline: tokio::time::Instant,
409) -> Result<(), String> {
410 if tokio::time::Instant::now() >= deadline {
411 return Err("Kafka drain deadline expired before provider execution".into());
412 }
413 execution
414 .compare_exchange(
415 KAFKA_DRAIN_EXECUTION_PENDING,
416 KAFKA_DRAIN_EXECUTION_STARTED,
417 Ordering::AcqRel,
418 Ordering::Acquire,
419 )
420 .map(|_| ())
421 .map_err(|state| match state {
422 KAFKA_DRAIN_EXECUTION_CANCELLED => {
423 "Kafka drain resolution was cancelled before execution".into()
424 }
425 _ => "Kafka drain resolution execution was already claimed".into(),
426 })
427}
428
429type KafkaPartitionSet = std::collections::HashSet<(String, i32)>;
430type KafkaPartitionBaselines = std::collections::HashMap<(String, i32), i64>;
431
432struct KafkaStartPlan {
433 config: KafkaSourceConfig,
434 delivery: DeliveryGuarantee,
435 is_resume: bool,
436 resume_inventory: Option<KafkaPartitionSet>,
437 resume_baselines: KafkaPartitionBaselines,
438}
439type KafkaPartitionRoutes = std::collections::HashMap<Arc<str>, Arc<[u32]>>;
440type KafkaRotationBaselines =
441 std::collections::HashMap<Arc<str>, std::collections::HashMap<i32, i64>>;
442
443fn kafka_drain_partitions(
444 assignment: &TopicPartitionList,
445) -> Result<Arc<[KafkaDrainPartition]>, ConnectorError> {
446 let mut inputs = Vec::with_capacity(assignment.count());
447 for element in assignment.elements() {
448 if element.topic().is_empty() {
449 return Err(ConnectorError::ConfigurationError(
450 "Kafka assignment contains an empty topic".into(),
451 ));
452 }
453 if element.partition() < 0 {
454 return Err(ConnectorError::ConfigurationError(format!(
455 "Kafka assignment contains invalid partition '{}-{}'",
456 element.topic(),
457 element.partition()
458 )));
459 }
460 inputs.push(KafkaDrainPartition {
461 topic: Arc::from(element.topic()),
462 partition: element.partition(),
463 });
464 }
465 inputs.sort_unstable_by(|left, right| {
466 left.topic
467 .as_ref()
468 .cmp(right.topic.as_ref())
469 .then(left.partition.cmp(&right.partition))
470 });
471 if let Some(duplicate) = inputs.windows(2).find(|pair| pair[0] == pair[1]) {
472 return Err(ConnectorError::ConfigurationError(format!(
473 "Kafka assignment contains duplicate input '{}-{}'",
474 duplicate[0].topic, duplicate[0].partition
475 )));
476 }
477 Ok(inputs.into())
478}
479
480fn kafka_partition_routes(
481 source_identity: &str,
482 vnode_count: u32,
483 topic_meta: &[(Arc<str>, i32)],
484) -> Result<KafkaPartitionRoutes, ConnectorError> {
485 let mut routes = KafkaPartitionRoutes::with_capacity(topic_meta.len());
486 for (topic, count) in topic_meta {
487 if *count < 0 {
488 return Err(ConnectorError::ConfigurationError(format!(
489 "Kafka topic '{topic}' reported a negative partition count {count}"
490 )));
491 }
492 let topic_routes =
493 super::vnode_routing::partition_vnodes(source_identity, topic, *count, vnode_count)?;
494 if routes
495 .insert(Arc::clone(topic), topic_routes.into())
496 .is_some()
497 {
498 return Err(ConnectorError::ConfigurationError(format!(
499 "Kafka vnode assignment contains duplicate topic '{topic}'"
500 )));
501 }
502 }
503 Ok(routes)
504}
505
506fn kafka_partition_set(tpl: &TopicPartitionList) -> Result<KafkaPartitionSet, String> {
507 let mut partitions = KafkaPartitionSet::with_capacity(tpl.count());
508 for element in tpl.elements() {
509 if element.partition() < 0 {
510 return Err(format!(
511 "Kafka assignment contains negative partition '{}-{}'",
512 element.topic(),
513 element.partition()
514 ));
515 }
516 let partition = (element.topic().to_string(), element.partition());
517 if !partitions.insert(partition) {
518 return Err(format!(
519 "Kafka assignment contains duplicate partition '{}-{}'",
520 element.topic(),
521 element.partition()
522 ));
523 }
524 }
525 Ok(partitions)
526}
527
528fn validate_kafka_assignment(
529 expected: &KafkaPartitionSet,
530 actual: &KafkaPartitionSet,
531) -> Result<(), String> {
532 if expected == actual {
533 return Ok(());
534 }
535 let missing = expected
536 .difference(actual)
537 .min()
538 .map(|(topic, partition)| format!("{topic}-{partition}"));
539 let unexpected = actual
540 .difference(expected)
541 .min()
542 .map(|(topic, partition)| format!("{topic}-{partition}"));
543 Err(format!(
544 "Kafka consumer assignment mismatch: expected {} partitions, found {}; first missing: {}, first unexpected: {}",
545 expected.len(),
546 actual.len(),
547 missing.as_deref().unwrap_or("none"),
548 unexpected.as_deref().unwrap_or("none")
549 ))
550}
551
552fn kafka_bootstrap_is_unassigned(
553 published: &laminar_core::state::VnodeAssignmentSnapshot,
554 self_id: laminar_core::state::NodeId,
555) -> Result<bool, ConnectorError> {
556 if self_id.is_unassigned() {
557 return Err(ConnectorError::ConfigurationError(
558 "Kafka vnode ownership requires a nonzero node identity".into(),
559 ));
560 }
561 if published.owners().is_empty() {
562 return Err(ConnectorError::ConfigurationError(
563 "Kafka vnode assignment cannot use an empty owner map".into(),
564 ));
565 }
566 if published.version() != 0 {
567 super::vnode_routing::validate_owner_map(published.owners(), self_id)?;
568 return Ok(false);
569 }
570 if published.has_committed_handoff()
571 || !published
572 .owners()
573 .iter()
574 .all(laminar_core::state::NodeId::is_unassigned)
575 {
576 return Err(ConnectorError::ConfigurationError(
577 "Kafka vnode assignment version 0 must be the fully unassigned bootstrap publication"
578 .into(),
579 ));
580 }
581 Ok(true)
582}
583
584fn kafka_owned_partition_sets(
585 routes: &KafkaPartitionRoutes,
586 published: &laminar_core::state::VnodeAssignmentSnapshot,
587 self_id: laminar_core::state::NodeId,
588 reconciled_version: u64,
589) -> Result<(KafkaPartitionSet, KafkaPartitionSet), ConnectorError> {
590 super::vnode_routing::validate_owner_map(published.owners(), self_id)?;
591 let mut owned = KafkaPartitionSet::new();
592 let mut reacquired = KafkaPartitionSet::new();
593 for (topic, topic_routes) in routes {
594 for (partition_index, vnode) in topic_routes.iter().copied().enumerate() {
595 let vnode_index = usize::try_from(vnode).map_err(|_| {
596 ConnectorError::ConfigurationError(
597 "Kafka vnode id cannot be represented on this platform".into(),
598 )
599 })?;
600 let assigned_owner = published.owners().get(vnode_index).ok_or_else(|| {
601 ConnectorError::ConfigurationError(format!(
602 "Kafka cached vnode {vnode} is outside owner map cardinality {}",
603 published.owners().len()
604 ))
605 })?;
606 if *assigned_owner != self_id {
607 continue;
608 }
609 let partition = i32::try_from(partition_index).map_err(|_| {
610 ConnectorError::ConfigurationError(format!(
611 "Kafka topic '{topic}' partition index cannot be represented as i32"
612 ))
613 })?;
614 let input = (topic.to_string(), partition);
615 if reconciled_version != 0
616 && published
617 .owner_changed_version(vnode)
618 .is_some_and(|changed| changed > reconciled_version)
619 {
620 reacquired.insert(input.clone());
621 }
622 owned.insert(input);
623 }
624 }
625 Ok((owned, reacquired))
626}
627
628fn kafka_assignment_fence_matches(
629 registry: &laminar_core::state::VnodeRegistry,
630 reconciled: &AtomicU64,
631 assignment_version: u64,
632) -> bool {
633 assignment_version != 0
634 && registry.assignment_version() == assignment_version
635 && reconciled.load(Ordering::Acquire) == assignment_version
636}
637
638fn try_capture_at_assignment_fence<T>(
639 registry: &laminar_core::state::VnodeRegistry,
640 reconciled: &AtomicU64,
641 assignment_publication: &Mutex<Arc<KafkaAssignmentPublication>>,
642 capture: impl FnOnce(&KafkaAssignmentPublication) -> Result<T, ConnectorError>,
643) -> Result<Option<T>, ConnectorError> {
644 let publication = {
646 let published = registry.read_assignment();
647 let version = published.version();
648 if version == 0 || reconciled.load(Ordering::Acquire) != version {
649 return Ok(None);
650 }
651 let publication = Arc::clone(&lock_or_recover(assignment_publication));
652 if publication.assignment_version != version {
653 return Ok(None);
654 }
655 publication
656 };
657 let captured = capture(&publication)?;
658 Ok(
659 kafka_assignment_fence_matches(registry, reconciled, publication.assignment_version)
660 .then_some(captured),
661 )
662}
663
664fn cached_partition_vnode(
665 topic_routes: Option<&[u32]>,
666 partition: i32,
667) -> Result<Option<u32>, ConnectorError> {
668 let Some(topic_routes) = topic_routes else {
669 return Ok(None);
670 };
671 let partition_index = usize::try_from(partition).map_err(|_| {
672 ConnectorError::ConfigurationError(format!(
673 "Kafka payload has negative partition id {partition}"
674 ))
675 })?;
676 topic_routes
677 .get(partition_index)
678 .copied()
679 .map(Some)
680 .ok_or_else(|| {
681 ConnectorError::ConfigurationError(format!(
682 "Kafka payload partition {partition} is outside the activated topic inventory {}",
683 topic_routes.len()
684 ))
685 })
686}
687
688fn kafka_partition_result_errors(tpl: &TopicPartitionList) -> Vec<String> {
689 tpl.elements()
690 .iter()
691 .filter_map(|element| {
692 element
693 .error()
694 .err()
695 .map(|error| format!("{}-{}: {error}", element.topic(), element.partition()))
696 })
697 .collect()
698}
699
700fn validate_kafka_partition_results(
701 operation: &str,
702 tpl: &TopicPartitionList,
703) -> Result<(), String> {
704 let errors = kafka_partition_result_errors(tpl);
705 validate_kafka_partition_error_list(operation, &errors)
706}
707
708fn validate_kafka_partition_error_list(operation: &str, errors: &[String]) -> Result<(), String> {
709 if errors.is_empty() {
710 Ok(())
711 } else {
712 Err(format!(
713 "Kafka {operation} failed for partitions: {}",
714 errors.join(", ")
715 ))
716 }
717}
718
719fn kafka_drain_target_ready(
720 target_version: u64,
721 registry_version: u64,
722 reconciled_version: u64,
723) -> Result<bool, String> {
724 if registry_version > target_version || reconciled_version > target_version {
725 return Err(format!(
726 "Kafka drain target {target_version} was superseded (registry {registry_version}, reconciled {reconciled_version})"
727 ));
728 }
729 if reconciled_version > registry_version {
730 return Err(format!(
731 "Kafka drain reconciliation {reconciled_version} is ahead of registry {registry_version}"
732 ));
733 }
734 Ok(registry_version == target_version && reconciled_version == target_version)
735}
736
737async fn resolve_kafka_reader_drain(
738 consumer: &Arc<StreamConsumer<LaminarConsumerContext>>,
739 blocking_tasks: &KafkaBlockingTasks,
740 vnode_reassign: Option<&(
741 Arc<laminar_core::state::VnodeRegistry>,
742 laminar_core::state::NodeId,
743 )>,
744 active: &KafkaReaderDrain,
745 resolution: SourceDrainResolution,
746 cut: &[KafkaDrainPosition],
747 globally_paused: bool,
748 deadline: tokio::time::Instant,
749 execution: &Arc<AtomicU8>,
750) -> Result<(), String> {
751 if resolution.round != active.request.round {
752 return Err("Kafka drain resolution does not match the active round".into());
753 }
754 let Some((registry, _)) = vnode_reassign else {
755 return Err("Kafka drain resolution has no cluster assignment".into());
756 };
757 if registry.assignment_version() != resolution.round.target_version {
758 return Err(format!(
759 "Kafka drain target {} is not installed (current {})",
760 resolution.round.target_version,
761 registry.assignment_version()
762 ));
763 }
764 let assignment = consumer
765 .assignment()
766 .map_err(|error| format!("Kafka drain could not inspect target assignment: {error}"))?;
767 if resolution.outcome == SourceDrainOutcome::Abort {
768 let assigned = kafka_partition_set(&assignment)
769 .map_err(|error| format!("Kafka drain target assignment is invalid: {error}"))?;
770 let mut seek = TopicPartitionList::new();
771 for position in cut
772 .iter()
773 .filter(|position| assigned.contains(&(position.topic.to_string(), position.partition)))
774 {
775 seek.add_partition_offset(
776 position.topic.as_ref(),
777 position.partition,
778 rdkafka::Offset::Offset(position.next_offset),
779 )
780 .map_err(|error| {
781 format!(
782 "Kafka drain could not build abort seek for '{}-{}': {error}",
783 position.topic, position.partition
784 )
785 })?;
786 }
787 if seek.count() > 0 {
788 let seek_consumer = Arc::clone(consumer);
789 let seek_execution = Arc::clone(execution);
790 let positioned = tokio::time::timeout_at(
791 deadline,
792 blocking_tasks.run(move || -> Result<_, String> {
793 claim_kafka_drain_execution(&seek_execution, deadline)?;
794 let timeout = deadline
795 .saturating_duration_since(tokio::time::Instant::now())
796 .min(std::time::Duration::from_secs(5));
797 if timeout.is_zero() {
798 return Err("Kafka drain deadline expired before abort seek".into());
799 }
800 seek_consumer
801 .seek_partitions(seek, timeout)
802 .map_err(|error| format!("Kafka drain abort seek failed: {error}"))
803 }),
804 )
805 .await
806 .map_err(|_| "Kafka drain deadline expired during abort seek".to_string())?
807 .map_err(|error| format!("Kafka drain abort seek task failed: {error}"))??;
808 validate_kafka_partition_results("drain abort seek", &positioned)?;
809 } else {
810 claim_kafka_drain_execution(execution, deadline)?;
811 }
812 } else {
813 claim_kafka_drain_execution(execution, deadline)?;
814 }
815 if tokio::time::Instant::now() >= deadline {
816 return Err("Kafka drain deadline expired before target resume".into());
817 }
818 if !globally_paused && assignment.count() > 0 {
819 consumer
820 .resume(&assignment)
821 .map_err(|error| format!("Kafka drain target resume failed: {error}"))?;
822 validate_kafka_partition_results("drain target resume", &assignment)?;
823 }
824 Ok(())
825}
826
827fn decode_committed_kafka_handoff(
828 handoff: &CommittedSourceHandoff,
829 source: &str,
830) -> Result<(OffsetTracker, KafkaPartitionBaselines), ConnectorError> {
831 let source_state = handoff.source(source).ok_or_else(|| {
832 ConnectorError::ConfigurationError(format!(
833 "committed checkpoint {:?} has no handoff state for Kafka source '{source}'",
834 handoff.attempt()
835 ))
836 })?;
837 let checkpoint = source_state.checkpoint();
838 let assignment_version = checkpoint.source_assignment_version.ok_or_else(|| {
839 ConnectorError::ConfigurationError(format!(
840 "Kafka source '{source}' handoff is missing its source assignment version"
841 ))
842 })?;
843 let checkpoint_assignment_version = NonZeroU64::new(handoff.checkpoint_assignment_version())
844 .ok_or_else(|| {
845 ConnectorError::ConfigurationError(format!(
846 "Kafka source '{source}' handoff has a zero checkpoint assignment version"
847 ))
848 })?;
849 if assignment_version != checkpoint_assignment_version {
850 return Err(ConnectorError::ConfigurationError(format!(
851 "Kafka source '{source}' handoff assignment version {assignment_version} does not match checkpoint fence {checkpoint_assignment_version}",
852 )));
853 }
854
855 let offsets = OffsetTracker::try_from_connector_checkpoint(checkpoint)?;
856 let baselines = decode_partition_baselines_from_offsets(&checkpoint.offsets)?;
857 Ok((offsets, baselines))
858}
859
860#[derive(Clone, Default)]
861struct KafkaAssignmentPublication {
862 assignment_version: u64,
863 owned_partitions: Arc<KafkaPartitionSet>,
864 baselines: KafkaRotationBaselines,
865}
866
867impl KafkaAssignmentPublication {
868 fn new(
869 assignment_version: u64,
870 owned_partitions: Arc<KafkaPartitionSet>,
871 baselines: KafkaRotationBaselines,
872 ) -> Self {
873 Self {
874 assignment_version,
875 owned_partitions,
876 baselines,
877 }
878 }
879}
880
881async fn join_background_task(
882 handle: &mut Option<tokio::task::JoinHandle<()>>,
883 deadline: tokio::time::Instant,
884 task: &'static str,
885) {
886 let Some(owned) = handle.as_mut() else {
887 return;
888 };
889 let completed = match tokio::time::timeout_at(deadline, &mut *owned).await {
890 Ok(Ok(())) => true,
891 Ok(Err(error)) => {
892 warn!(task, %error, "Kafka background task failed during shutdown");
893 true
894 }
895 Err(_) => {
896 warn!(
897 task,
898 "Kafka background task shutdown timed out; aborting it"
899 );
900 owned.abort();
901 false
902 }
903 };
904 if completed {
905 *handle = None;
906 }
907}
908
909fn ensure_background_task_reaper(
910 handle: tokio::task::JoinHandle<()>,
911 task_owner: &ConnectorTaskOwner,
912 task: &'static str,
913) {
914 handle.abort();
915 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
916 warn!(
917 task,
918 "Kafka background task retired outside a Tokio runtime"
919 );
920 return;
921 };
922 let Some(terminal_guard) = task_owner.track() else {
923 warn!(
924 task,
925 "Kafka task generation was sealed before reader reaping"
926 );
927 return;
928 };
929 drop(runtime.spawn(async move {
930 let _terminal_guard = terminal_guard;
931 if let Err(error) = handle.await {
932 debug!(task, %error, "Kafka retired background task reaped");
933 }
934 }));
935}
936
937async fn reap_last_arc_off_runtime<T: Send + Sync + 'static>(
941 blocking_tasks: &KafkaBlockingTasks,
942 owner: Arc<T>,
943 deadline: tokio::time::Instant,
944 resource: &'static str,
945) {
946 let reaper = blocking_tasks.run(move || {
947 while Arc::strong_count(&owner) > 1 {
948 std::thread::sleep(std::time::Duration::from_millis(1));
949 }
950 drop(owner);
951 });
952 match tokio::time::timeout_at(deadline, reaper).await {
953 Ok(Ok(())) => {}
954 Ok(Err(error)) => warn!(resource, %error, "Kafka blocking reaper failed"),
955 Err(_) => warn!(
956 resource,
957 "Kafka resource cleanup exceeded close deadline; generation reaper retained it"
958 ),
959 }
960}
961
962type KafkaReaderRx = crossfire::AsyncRx<crossfire::mpsc::Array<KafkaReaderItem>>;
964
965pub struct KafkaSource {
983 consumer: Option<Arc<StreamConsumer<LaminarConsumerContext>>>,
984 config: KafkaSourceConfig,
985 delivery: DeliveryGuarantee,
988 deserializer: Box<dyn RecordDeserializer>,
989 offsets: OffsetTracker,
990 state: ConnectorState,
991 metrics: KafkaSourceMetrics,
992 schema: SchemaRef,
993 channel_len: Arc<AtomicUsize>,
994 rebalance_state: Arc<Mutex<RebalanceState>>,
995 rebalance_counter: Arc<AtomicU64>,
997 revoke_generation: Arc<AtomicU64>,
1000 assign_generation: Arc<AtomicU64>,
1003 last_seen_revoke_gen: u64,
1004 schema_registry: Option<Arc<SchemaRegistryClient>>,
1005 data_ready: Arc<Notify>,
1006 msg_rx: Option<KafkaReaderRx>,
1007 reader_handle: Option<tokio::task::JoinHandle<()>>,
1008 reader_shutdown: Option<tokio::sync::watch::Sender<bool>>,
1009 task_owner: ConnectorTaskOwner,
1011 task_tracker: ConnectorTaskTracker,
1013 blocking_tasks: KafkaBlockingTasks,
1015 reader_fault: Arc<Mutex<Option<Arc<str>>>>,
1017 reader_drain_tx: Option<tokio::sync::mpsc::UnboundedSender<KafkaReaderDrainCommand>>,
1019 source_drain: Option<KafkaSourceDrain>,
1020 offset_snapshot: Arc<Mutex<OffsetTracker>>,
1023 deterministic_unrecorded_position: Arc<AtomicBool>,
1029
1030 source_name: Arc<str>,
1032
1033 vnode_assignment: Option<(
1037 Arc<laminar_core::state::VnodeRegistry>,
1038 laminar_core::state::NodeId,
1039 )>,
1040 vnode_partition_routes: KafkaPartitionRoutes,
1042 manual_topic_partitions: KafkaPartitionSet,
1045 manual_partition_baselines: KafkaPartitionBaselines,
1048 assignment_publication: Arc<Mutex<Arc<KafkaAssignmentPublication>>>,
1050 rotation_partition_baseline_count: Arc<AtomicUsize>,
1053 applied_rotation_baseline_version: Option<u64>,
1057 reconciled_assignment_version: Arc<AtomicU64>,
1061
1062 last_avro_schema: Option<SchemaRef>,
1064
1065 poll_payloads: Vec<KafkaPayload>,
1067 poll_payload_buf: Vec<u8>,
1068 poll_payload_offsets: Vec<(usize, usize)>,
1069 poll_meta_partitions: Vec<i32>,
1070 poll_meta_offsets: Vec<i64>,
1071 poll_meta_timestamps: Vec<Option<i64>>,
1072 poll_meta_headers: Vec<Option<String>>,
1073 poll_staged_offsets: Vec<(Arc<str>, i32, i64)>,
1077}
1078
1079impl KafkaSource {
1080 #[must_use]
1082 pub fn new(
1083 schema: SchemaRef,
1084 config: KafkaSourceConfig,
1085 registry: Option<&prometheus::Registry>,
1086 ) -> Self {
1087 Self::build_base(schema, config, select_deserializer, None, registry)
1088 }
1089
1090 #[must_use]
1092 pub fn with_schema_registry(
1093 schema: SchemaRef,
1094 config: KafkaSourceConfig,
1095 sr_client: SchemaRegistryClient,
1096 ) -> Self {
1097 let sr = Arc::new(sr_client);
1098 let sr_clone = Arc::clone(&sr);
1099 let deser_factory = move |format: Format| -> Box<dyn RecordDeserializer> {
1100 if format == Format::Avro {
1101 Box::new(AvroDeserializer::with_schema_registry(sr_clone))
1102 } else {
1103 select_deserializer(format)
1104 }
1105 };
1106 Self::build_base(schema, config, deser_factory, Some(sr), None)
1107 }
1108
1109 fn build_sr_client(
1112 config: &KafkaSourceConfig,
1113 ) -> Result<Option<SchemaRegistryClient>, ConnectorError> {
1114 let Some(sr_url) = config.schema_registry_url.as_ref() else {
1115 return Ok(None);
1116 };
1117 let client = if let Some(ca) = config.schema_registry_ssl_ca_location.as_deref() {
1118 SchemaRegistryClient::with_tls_mtls(
1119 sr_url.clone(),
1120 config.schema_registry_auth.clone(),
1121 ca,
1122 config.schema_registry_ssl_certificate_location.as_deref(),
1123 config.schema_registry_ssl_key_location.as_deref(),
1124 )?
1125 } else {
1126 SchemaRegistryClient::new(sr_url.clone(), config.schema_registry_auth.clone())?
1127 };
1128 Ok(Some(client))
1129 }
1130
1131 fn build_base(
1132 schema: SchemaRef,
1133 config: KafkaSourceConfig,
1134 deser_factory: impl FnOnce(Format) -> Box<dyn RecordDeserializer>,
1135 schema_registry: Option<Arc<SchemaRegistryClient>>,
1136 registry: Option<&prometheus::Registry>,
1137 ) -> Self {
1138 let deserializer = deser_factory(config.format);
1139 let channel_len = Arc::new(AtomicUsize::new(0));
1140 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
1141 let blocking_guard = task_owner
1142 .track()
1143 .expect("new Kafka source task generation must be live");
1144
1145 Self {
1146 consumer: None,
1147 config,
1148 delivery: DeliveryGuarantee::BestEffort,
1149 deserializer,
1150 offsets: OffsetTracker::new(),
1151 state: ConnectorState::Created,
1152 metrics: KafkaSourceMetrics::new(registry),
1153 schema,
1154 channel_len,
1155 rebalance_state: Arc::new(Mutex::new(RebalanceState::new())),
1156 rebalance_counter: Arc::new(AtomicU64::new(0)),
1157 revoke_generation: Arc::new(AtomicU64::new(0)),
1158 assign_generation: Arc::new(AtomicU64::new(0)),
1159 last_seen_revoke_gen: 0,
1160 schema_registry,
1161 data_ready: Arc::new(Notify::new()),
1162 msg_rx: None,
1163 reader_handle: None,
1164 reader_shutdown: None,
1165 task_owner,
1166 task_tracker,
1167 blocking_tasks: KafkaBlockingTasks::new(blocking_guard),
1168 reader_fault: Arc::new(Mutex::new(None)),
1169 reader_drain_tx: None,
1170 source_drain: None,
1171 offset_snapshot: Arc::new(Mutex::new(OffsetTracker::new())),
1172 deterministic_unrecorded_position: Arc::new(AtomicBool::new(false)),
1173 source_name: Arc::from(""),
1174 vnode_assignment: None,
1175 vnode_partition_routes: KafkaPartitionRoutes::new(),
1176 manual_topic_partitions: std::collections::HashSet::new(),
1177 manual_partition_baselines: std::collections::HashMap::new(),
1178 assignment_publication: Arc::new(Mutex::new(Arc::new(
1179 KafkaAssignmentPublication::default(),
1180 ))),
1181 rotation_partition_baseline_count: Arc::new(AtomicUsize::new(0)),
1182 applied_rotation_baseline_version: None,
1183 reconciled_assignment_version: Arc::new(AtomicU64::new(0)),
1184 last_avro_schema: None,
1185 poll_payloads: Vec::new(),
1186 poll_payload_buf: Vec::new(),
1187 poll_payload_offsets: Vec::new(),
1188 poll_meta_partitions: Vec::new(),
1189 poll_meta_offsets: Vec::new(),
1190 poll_meta_timestamps: Vec::new(),
1191 poll_meta_headers: Vec::new(),
1192 poll_staged_offsets: Vec::new(),
1193 }
1194 }
1195
1196 #[must_use]
1198 pub fn state(&self) -> ConnectorState {
1199 self.state
1200 }
1201
1202 #[must_use]
1204 pub fn channel_len(&self) -> Arc<AtomicUsize> {
1205 Arc::clone(&self.channel_len)
1206 }
1207
1208 #[must_use]
1210 pub fn rebalance_state(&self) -> Arc<Mutex<RebalanceState>> {
1211 Arc::clone(&self.rebalance_state)
1212 }
1213
1214 #[must_use]
1216 pub fn has_schema_registry(&self) -> bool {
1217 self.schema_registry.is_some()
1218 }
1219
1220 fn fail_startup(&mut self) {
1221 self.state = ConnectorState::Failed;
1222 self.blocking_tasks.retire();
1223 self.blocking_tasks.ensure_reaper();
1224 if let Some(consumer) = self.consumer.take() {
1225 consumer.unsubscribe();
1226 self.blocking_tasks
1227 .spawn_final_drop(consumer, "consumer after failed startup");
1228 }
1229 }
1230
1231 fn capture_drain_positions(
1232 &self,
1233 inputs: &[KafkaDrainPartition],
1234 prepare_deadline: Option<tokio::time::Instant>,
1235 ) -> Result<Arc<[KafkaDrainPosition]>, ConnectorError> {
1236 if prepare_deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
1237 return Err(ConnectorError::Internal(
1238 "Kafka drain deadline expired before cursor capture".into(),
1239 ));
1240 }
1241 let publication = Arc::clone(&lock_or_recover(&self.assignment_publication));
1242 let mut cut = Vec::with_capacity(inputs.len());
1243 for input in inputs {
1244 if prepare_deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
1245 return Err(ConnectorError::Internal(
1246 "Kafka drain deadline expired during cursor capture".into(),
1247 ));
1248 }
1249 let next_offset =
1250 if let Some(offset) = self.offsets.get(input.topic.as_ref(), input.partition) {
1251 offset.checked_add(1).ok_or_else(|| {
1252 ConnectorError::Internal(format!(
1253 "Kafka drain offset overflow for '{}-{}'",
1254 input.topic, input.partition
1255 ))
1256 })?
1257 } else if let Some(next) = rotation_partition_baseline(
1258 &publication.baselines,
1259 input.topic.as_ref(),
1260 input.partition,
1261 ) {
1262 next
1263 } else if let Some(next) = self
1264 .manual_partition_baselines
1265 .get(&(input.topic.to_string(), input.partition))
1266 {
1267 *next
1268 } else {
1269 return Err(ConnectorError::InvalidState {
1270 expected: format!(
1271 "numeric next-to-read position for drained Kafka input '{}-{}'",
1272 input.topic, input.partition
1273 ),
1274 actual: "no accepted offset or deterministic baseline".into(),
1275 });
1276 };
1277 cut.push(KafkaDrainPosition {
1278 topic: Arc::clone(&input.topic),
1279 partition: input.partition,
1280 next_offset,
1281 });
1282 }
1283 if prepare_deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
1284 return Err(ConnectorError::Internal(
1285 "Kafka drain deadline expired during cursor capture".into(),
1286 ));
1287 }
1288 Ok(cut.into())
1289 }
1290
1291 fn check_reader_health(&self, operation: &str) -> Result<(), ConnectorError> {
1292 if let Some(reason) = lock_or_recover(&self.reader_fault).clone() {
1293 return Err(ConnectorError::Internal(format!(
1294 "Kafka reader terminated while {operation}: {reason}"
1295 )));
1296 }
1297 if self
1298 .reader_handle
1299 .as_ref()
1300 .is_some_and(tokio::task::JoinHandle::is_finished)
1301 {
1302 return Err(ConnectorError::Internal(format!(
1303 "Kafka reader task exited while {operation}"
1304 )));
1305 }
1306 Ok(())
1307 }
1308
1309 fn validate_active_drain_cursor(&self) -> Result<(), ConnectorError> {
1310 self.check_reader_health("validating a drain cursor")?;
1311 let Some(active) = self.source_drain.as_ref() else {
1312 return Ok(());
1313 };
1314 let Some(expected) = active.cut.as_deref() else {
1315 return Ok(());
1316 };
1317 let Some(boundary) = active.boundary.as_ref() else {
1318 return Err(ConnectorError::Internal(
1319 "Kafka drain cut exists without its FIFO boundary".into(),
1320 ));
1321 };
1322 let current = self.capture_drain_positions(&boundary.inputs, None)?;
1325 if current.as_ref() != expected {
1326 return Err(ConnectorError::Internal(
1327 "Kafka input cursor advanced after its drain receipt".into(),
1328 ));
1329 }
1330 Ok(())
1331 }
1332
1333 fn ensure_reader_started(&mut self) {
1337 if self.reader_handle.is_some() || self.consumer.is_none() {
1338 return;
1339 }
1340
1341 let consumer = Arc::clone(self.consumer.as_ref().unwrap());
1342 let vnode_reassign = self
1345 .vnode_assignment
1346 .as_ref()
1347 .map(|(r, s)| (Arc::clone(r), *s));
1348 let (msg_tx, msg_rx) =
1349 crossfire::mpsc::bounded_async::<KafkaReaderItem>(self.config.reader_channel_capacity);
1350 let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
1351 let (reader_drain_tx, mut reader_drain_rx) = if vnode_reassign.is_some() {
1352 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1353 (Some(tx), Some(rx))
1354 } else {
1355 (None, None)
1356 };
1357 let data_ready = Arc::clone(&self.data_ready);
1358 let reader_fault = Arc::clone(&self.reader_fault);
1359 let channel_len = Arc::clone(&self.channel_len);
1360 let capture_headers = self.config.include_headers;
1361 let reader_channel_capacity = self.config.reader_channel_capacity;
1362 let reader_channel_capacity_f64 =
1363 u32::try_from(reader_channel_capacity).map_or(f64::from(u32::MAX), f64::from);
1364 let assign_generation = Arc::clone(&self.assign_generation);
1365 let rebalance_state = Arc::clone(&self.rebalance_state);
1366 let pause_threshold = self.config.backpressure_high_watermark;
1367 let resume_threshold = self.config.backpressure_low_watermark;
1368
1369 let vnode_partition_routes = std::mem::take(&mut self.vnode_partition_routes);
1372 let reassign_snapshot = Arc::clone(&self.offset_snapshot);
1373 let reassign_baselines = self.manual_partition_baselines.clone();
1374 let assignment_publication = Arc::clone(&self.assignment_publication);
1375 let rotation_baseline_count = Arc::clone(&self.rotation_partition_baseline_count);
1376 let reconciled_assignment_version = Arc::clone(&self.reconciled_assignment_version);
1377 let require_durable_baselines = !reassign_baselines.is_empty();
1378 let reassign_default_offset = startup_default_offset(&self.config.startup_mode);
1379 let deterministic_unrecorded = Arc::clone(&self.deterministic_unrecorded_position);
1380 let source_name = Arc::clone(&self.source_name);
1381 let blocking_tasks = self.blocking_tasks.clone();
1382 let deterministic_default =
1383 deterministic_initial_offset(&self.config.startup_mode, self.config.auto_offset_reset);
1384 let mut reader_shutdown = shutdown_rx;
1385 let reader_guard = self
1386 .task_owner
1387 .track()
1388 .expect("live Kafka source must admit its reader task");
1389 let reader_handle = tokio::spawn(async move {
1390 let _reader_guard = reader_guard;
1391 let mut cached_topic: Arc<str> = Arc::from("");
1392 let mut cached_topic_routes: Option<Arc<[u32]>> = None;
1393 let mut is_paused = false;
1394 let mut last_assign_gen: u64 = 0;
1395 let mut last_assignment_version = reconciled_assignment_version.load(Ordering::Acquire);
1399 let mut drain_paused: std::collections::HashSet<(Arc<str>, i32)> =
1400 std::collections::HashSet::new();
1401 let mut active_drain: Option<KafkaReaderDrain> = None;
1402 let mut deferred_drain_command = None;
1403
1404 loop {
1405 if active_drain.as_ref().is_some_and(|active| {
1406 !active.boundary_queued
1407 && tokio::time::Instant::now() >= active.prepare_deadline
1408 }) {
1409 publish_reader_fault(
1410 &reader_fault,
1411 &data_ready,
1412 "Kafka source drain preparation exceeded its engine deadline",
1413 );
1414 return;
1415 }
1416 if let Some((registry, self_id)) = &vnode_reassign {
1421 let published = registry.versioned_snapshot();
1422 let version = published.version();
1423 if version != last_assignment_version {
1424 let current = match consumer.assignment() {
1425 Ok(current) => current,
1426 Err(error) => {
1427 warn!(
1428 version,
1429 %error,
1430 "Kafka source could not inspect its current assignment; rotation will retry"
1431 );
1432 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1433 continue;
1434 }
1435 };
1436 let current_set = match kafka_partition_set(¤t) {
1441 Ok(current) => current,
1442 Err(error) => {
1443 publish_reader_fault(&reader_fault, &data_ready, error);
1444 return;
1445 }
1446 };
1447 let (owned_set, reacquired) = match kafka_owned_partition_sets(
1448 &vnode_partition_routes,
1449 &published,
1450 *self_id,
1451 last_assignment_version,
1452 ) {
1453 Ok(partitions) => partitions,
1454 Err(error) => {
1455 warn!(
1456 source = source_name.as_ref(),
1457 %error,
1458 "Kafka source rejected its cached partition routes"
1459 );
1460 publish_reader_fault(
1461 &reader_fault,
1462 &data_ready,
1463 format!("invalid cached partition route: {error}"),
1464 );
1465 return;
1466 }
1467 };
1468
1469 let mut to_remove = TopicPartitionList::new();
1470 for (topic, partition) in current_set
1471 .difference(&owned_set)
1472 .chain(reacquired.intersection(¤t_set))
1473 {
1474 to_remove.add_partition(topic, *partition);
1475 }
1476
1477 let offsets = lock_or_recover(&reassign_snapshot).clone();
1478 let (resume, resume_baselines) = if let Some(handoff) =
1479 published.committed_source_handoff()
1480 {
1481 match decode_committed_kafka_handoff(handoff, source_name.as_ref()) {
1482 Ok(state) => state,
1483 Err(error) => {
1484 warn!(
1485 version,
1486 source = source_name.as_ref(),
1487 error = %error,
1488 "Kafka source rejected durable checkpoint handoff"
1489 );
1490 publish_reader_fault(
1491 &reader_fault,
1492 &data_ready,
1493 format!("invalid durable checkpoint handoff: {error}"),
1494 );
1495 return;
1496 }
1497 }
1498 } else {
1499 (OffsetTracker::new(), KafkaPartitionBaselines::new())
1500 };
1501 let mut to_add = TopicPartitionList::new();
1502 let mut acquired_positions = KafkaPartitionBaselines::new();
1503 for (topic, partition) in owned_set
1504 .difference(¤t_set)
1505 .chain(reacquired.intersection(¤t_set))
1506 {
1507 let p = *partition;
1508 let offset = if let Some(next) = match acquired_numeric_position(
1509 &resume,
1510 &resume_baselines,
1511 &offsets,
1512 &reassign_baselines,
1513 topic.as_str(),
1514 p,
1515 !require_durable_baselines || !published.has_committed_handoff(),
1516 ) {
1517 Ok(position) => position,
1518 Err(error) => {
1519 warn!(
1520 topic = topic.as_str(),
1521 partition = p,
1522 %error,
1523 "Kafka source rejected an invalid handoff position"
1524 );
1525 publish_reader_fault(
1526 &reader_fault,
1527 &data_ready,
1528 format!("invalid handoff position: {error}"),
1529 );
1530 return;
1531 }
1532 } {
1533 info!(
1534 topic = topic.as_str(),
1535 partition = p,
1536 resume = next,
1537 "acquired partition uses durable numeric position"
1538 );
1539 acquired_positions.insert((topic.clone(), p), next);
1540 rdkafka::Offset::Offset(next)
1541 } else if require_durable_baselines {
1542 warn!(
1543 topic = topic.as_str(),
1544 partition = p,
1545 "acquired Kafka partition has no durable next-to-read baseline"
1546 );
1547 publish_reader_fault(
1548 &reader_fault,
1549 &data_ready,
1550 "acquired partition has no durable baseline",
1551 );
1552 return;
1553 } else if deterministic_unrecorded.load(Ordering::Acquire) {
1554 let Some(initial_offset) = deterministic_default else {
1555 warn!(
1556 topic = topic.as_str(),
1557 partition = p,
1558 "cannot deterministically position acquired Kafka partition"
1559 );
1560 publish_reader_fault(
1561 &reader_fault,
1562 &data_ready,
1563 "acquired partition has no deterministic position",
1564 );
1565 return;
1566 };
1567 initial_offset
1568 } else {
1569 warn!(
1570 topic = topic.as_str(),
1571 partition = p,
1572 "acquired partition has no handoff or local offset; \
1573 falling back to the startup default"
1574 );
1575 reassign_default_offset
1576 };
1577 if let Err(error) =
1578 to_add.add_partition_offset(topic.as_str(), p, offset)
1579 {
1580 warn!(
1581 topic = topic.as_str(),
1582 partition = p,
1583 %error,
1584 "failed to build Kafka rotation assignment"
1585 );
1586 publish_reader_fault(
1587 &reader_fault,
1588 &data_ready,
1589 format!("invalid rotation assignment: {error}"),
1590 );
1591 return;
1592 }
1593 }
1594
1595 let owned_set = Arc::new(owned_set);
1596
1597 {
1601 let mut current = lock_or_recover(&assignment_publication);
1602 let updated = update_rotation_baselines(
1603 ¤t.baselines,
1604 &owned_set,
1605 &acquired_positions,
1606 );
1607 let count = rotation_baselines_len(&updated);
1608 *current = Arc::new(KafkaAssignmentPublication::new(
1609 version,
1610 Arc::clone(&owned_set),
1611 updated,
1612 ));
1613 rotation_baseline_count.store(count, Ordering::Release);
1614 }
1615
1616 if !acquired_positions.is_empty() {
1617 let acquired: KafkaPartitionSet =
1618 acquired_positions.keys().cloned().collect();
1619 let low_watermarks = match fetch_partition_low_watermarks(
1620 blocking_tasks.clone(),
1621 Arc::clone(&consumer),
1622 &acquired,
1623 )
1624 .await
1625 {
1626 Ok(low_watermarks) => low_watermarks,
1627 Err(error) => {
1628 warn!(
1629 version,
1630 %error,
1631 "Kafka source could not validate acquired handoff positions; rotation will retry"
1632 );
1633 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1634 continue;
1635 }
1636 };
1637 if let Err(error) = validate_positions_not_expired(
1638 &OffsetTracker::new(),
1639 &acquired_positions,
1640 &low_watermarks,
1641 &acquired,
1642 ) {
1643 warn!(
1644 version,
1645 %error,
1646 "Kafka source rejected an expired handoff position"
1647 );
1648 publish_reader_fault(
1649 &reader_fault,
1650 &data_ready,
1651 format!("expired handoff position: {error}"),
1652 );
1653 return;
1654 }
1655 }
1656
1657 let mut ok = true;
1658 if to_remove.count() > 0 {
1659 match consumer.incremental_unassign(&to_remove) {
1660 Ok(()) => {
1661 if let Err(error) = validate_kafka_partition_results(
1662 "incremental unassign",
1663 &to_remove,
1664 ) {
1665 warn!(version, %error, "Kafka source unassign incomplete");
1666 ok = false;
1667 }
1668 }
1669 Err(e) => {
1670 warn!(version, error = %e, "Kafka source unassign failed");
1671 ok = false;
1672 }
1673 }
1674 }
1675 if to_add.count() > 0 {
1676 match consumer.incremental_assign(&to_add) {
1677 Ok(()) => {
1678 if let Err(error) = validate_kafka_partition_results(
1679 "incremental assign",
1680 &to_add,
1681 ) {
1682 warn!(version, %error, "Kafka source assign incomplete");
1683 ok = false;
1684 }
1685 }
1686 Err(e) => {
1687 warn!(version, error = %e, "Kafka source assign failed");
1688 ok = false;
1689 }
1690 }
1691 }
1692 if ok {
1693 match consumer.assignment() {
1694 Ok(active) => {
1695 match kafka_partition_set(&active).and_then(|active| {
1696 validate_kafka_assignment(&owned_set, &active)
1697 }) {
1698 Ok(()) => {}
1699 Err(error) => {
1700 warn!(
1701 version,
1702 %error,
1703 "Kafka source assignment verification failed"
1704 );
1705 ok = false;
1706 }
1707 }
1708 }
1709 Err(error) => {
1710 warn!(
1711 version,
1712 %error,
1713 "Kafka source could not verify its rebound assignment"
1714 );
1715 ok = false;
1716 }
1717 }
1718 }
1719 if !ok {
1720 match tokio::time::timeout(
1721 std::time::Duration::from_millis(10),
1722 reader_shutdown.changed(),
1723 )
1724 .await
1725 {
1726 Ok(Ok(())) if *reader_shutdown.borrow() => break,
1727 Ok(Err(_)) => break,
1728 _ => {}
1729 }
1730 continue;
1731 }
1732
1733 let current_assignment = registry.read_assignment();
1735 if current_assignment.version() != version {
1736 continue;
1737 }
1738 last_assignment_version = version;
1739 reconciled_assignment_version.store(version, Ordering::Release);
1740 drop(current_assignment);
1741 if to_remove.count() > 0 || to_add.count() > 0 {
1742 info!(
1743 version,
1744 acquired = to_add.count(),
1745 revoked = to_remove.count(),
1746 "Kafka source rebound partitions after vnode rotation"
1747 );
1748 }
1749 for element in to_add.elements() {
1752 drain_paused.remove(&(Arc::from(element.topic()), element.partition()));
1753 }
1754 drain_paused.retain(|(t, p)| owned_set.contains(&(t.to_string(), *p)));
1755 if let Some(active) = active_drain.as_mut() {
1756 active.held_assignment_version = None;
1757 active.hold_complete = false;
1758 }
1759 lock_or_recover(&reassign_snapshot).retain_assigned(&owned_set);
1761 data_ready.notify_one();
1762 }
1763 }
1764
1765 let command = deferred_drain_command.take().or_else(|| {
1768 reader_drain_rx
1769 .as_mut()
1770 .and_then(|receiver| receiver.try_recv().ok())
1771 });
1772 if let Some(command) = command {
1773 match command {
1774 KafkaReaderDrainCommand::Begin { request, deadline } => {
1775 if let Some(current) = active_drain.as_ref() {
1776 if current.request != request {
1777 warn!(
1778 current = ?current.request.round,
1779 requested = ?request.round,
1780 "Kafka reader received a conflicting drain round"
1781 );
1782 publish_reader_fault(
1783 &reader_fault,
1784 &data_ready,
1785 "conflicting Kafka drain round",
1786 );
1787 return;
1788 }
1789 } else {
1790 if tokio::time::Instant::now() >= deadline {
1791 publish_reader_fault(
1792 &reader_fault,
1793 &data_ready,
1794 "Kafka source drain began after its engine deadline",
1795 );
1796 return;
1797 }
1798 let Some((registry, _)) = vnode_reassign.as_ref() else {
1799 warn!("Kafka reader received drain control without cluster ownership");
1800 publish_reader_fault(
1801 &reader_fault,
1802 &data_ready,
1803 "drain control has no cluster ownership",
1804 );
1805 return;
1806 };
1807 if registry.assignment_version()
1808 != request.round.predecessor_version
1809 {
1810 warn!(
1811 current = registry.assignment_version(),
1812 predecessor = request.round.predecessor_version,
1813 "Kafka reader rejected drain for a stale predecessor"
1814 );
1815 publish_reader_fault(
1816 &reader_fault,
1817 &data_ready,
1818 "drain predecessor does not match current assignment",
1819 );
1820 return;
1821 }
1822 if last_assignment_version != request.round.predecessor_version {
1823 publish_reader_fault(
1824 &reader_fault,
1825 &data_ready,
1826 "Kafka drain predecessor is not reconciled",
1827 );
1828 return;
1829 }
1830 let assignment = match consumer.assignment() {
1831 Ok(assignment) => assignment,
1832 Err(error) => {
1833 publish_reader_fault(
1834 &reader_fault,
1835 &data_ready,
1836 format!(
1837 "Kafka drain could not inspect its assignment: {error}"
1838 ),
1839 );
1840 return;
1841 }
1842 };
1843 let inputs = match kafka_drain_partitions(&assignment) {
1844 Ok(inputs) => inputs,
1845 Err(error) => {
1846 warn!(
1847 source = source_name.as_ref(),
1848 %error,
1849 "Kafka reader rejected its drain assignment"
1850 );
1851 publish_reader_fault(
1852 &reader_fault,
1853 &data_ready,
1854 format!("invalid Kafka drain assignment: {error}"),
1855 );
1856 return;
1857 }
1858 };
1859 active_drain = Some(KafkaReaderDrain {
1860 request,
1861 prepare_deadline: deadline,
1862 held_inputs: Arc::clone(&inputs),
1863 inputs,
1864 held_assignment_version: Some(last_assignment_version),
1865 hold_complete: false,
1866 boundary_queued: false,
1867 });
1868 }
1869 }
1870 KafkaReaderDrainCommand::Resolve {
1871 resolution,
1872 cut,
1873 deadline,
1874 execution,
1875 reply,
1876 } => {
1877 if active_drain.is_none() {
1878 let _ = reply.send(Err(
1879 "Kafka reader has no active drain to resolve".into(),
1880 ));
1881 continue;
1882 }
1883 if tokio::time::Instant::now() >= deadline {
1884 let _ = reply.send(Err(
1885 "Kafka drain deadline expired before target reconciliation"
1886 .into(),
1887 ));
1888 continue;
1889 }
1890 let Some((registry, _)) = vnode_reassign.as_ref() else {
1891 let _ = reply.send(Err(
1892 "Kafka drain resolution has no cluster assignment".into(),
1893 ));
1894 continue;
1895 };
1896 let target_ready = match kafka_drain_target_ready(
1897 resolution.round.target_version,
1898 registry.assignment_version(),
1899 last_assignment_version,
1900 ) {
1901 Ok(ready) => ready,
1902 Err(error) => {
1903 let _ = reply.send(Err(error));
1904 continue;
1905 }
1906 };
1907 let target_paused = target_ready
1908 && active_drain.as_ref().is_some_and(|active| {
1909 active.held_assignment_version
1910 == Some(resolution.round.target_version)
1911 && active.hold_complete
1912 });
1913 if !target_ready || !target_paused {
1914 deferred_drain_command = Some(KafkaReaderDrainCommand::Resolve {
1915 resolution,
1916 cut,
1917 deadline,
1918 execution,
1919 reply,
1920 });
1921 } else {
1922 let result = resolve_kafka_reader_drain(
1923 &consumer,
1924 &blocking_tasks,
1925 vnode_reassign.as_ref(),
1926 active_drain.as_ref().expect("validated above"),
1927 resolution,
1928 &cut,
1929 is_paused,
1930 deadline,
1931 &execution,
1932 )
1933 .await;
1934 if result.is_ok() {
1935 active_drain = None;
1936 drain_paused.clear();
1937 }
1938 let _ = reply.send(result);
1939 continue;
1940 }
1941 }
1942 }
1943 }
1944
1945 if let Some(active) = active_drain.as_mut() {
1949 if active.held_assignment_version != Some(last_assignment_version) {
1950 let assignment = match consumer.assignment() {
1951 Ok(assignment) => assignment,
1952 Err(error) => {
1953 warn!(%error, "Kafka drain could not inspect the live assignment");
1954 continue;
1955 }
1956 };
1957 active.held_inputs = match kafka_drain_partitions(&assignment) {
1958 Ok(inputs) => inputs,
1959 Err(error) => {
1960 publish_reader_fault(
1961 &reader_fault,
1962 &data_ready,
1963 format!("invalid Kafka assignment while draining: {error}"),
1964 );
1965 return;
1966 }
1967 };
1968 let current_set: std::collections::HashSet<(Arc<str>, i32)> = active
1969 .held_inputs
1970 .iter()
1971 .map(|input| (Arc::clone(&input.topic), input.partition))
1972 .collect();
1973 drain_paused.retain(|input| current_set.contains(input));
1974 active.held_assignment_version = Some(last_assignment_version);
1975 active.hold_complete = false;
1976 }
1977 if !active.hold_complete {
1978 let remaining: Vec<(Arc<str>, i32)> = active
1979 .held_inputs
1980 .iter()
1981 .filter(|input| {
1982 !drain_paused.contains(&(Arc::clone(&input.topic), input.partition))
1983 })
1984 .map(|input| (Arc::clone(&input.topic), input.partition))
1985 .collect();
1986 let to_pause = tpl_of(remaining.iter());
1987 if to_pause.count() > 0 {
1988 match consumer.pause(&to_pause) {
1989 Ok(()) => {
1990 for element in to_pause.elements() {
1991 if element.error().is_ok() {
1992 drain_paused.insert((
1993 Arc::from(element.topic()),
1994 element.partition(),
1995 ));
1996 }
1997 }
1998 if let Err(error) =
1999 validate_kafka_partition_results("drain pause", &to_pause)
2000 {
2001 warn!(%error, "Kafka drain pause incomplete; will retry");
2002 continue;
2003 }
2004 }
2005 Err(error) => {
2006 warn!(%error, "Kafka drain pause failed; will retry");
2007 continue;
2008 }
2009 }
2010 }
2011 active.hold_complete = active.held_inputs.iter().all(|input| {
2012 drain_paused.contains(&(Arc::clone(&input.topic), input.partition))
2013 });
2014 }
2015 if !active.boundary_queued {
2016 let cut_is_paused = active.inputs.iter().all(|input| {
2017 drain_paused.contains(&(Arc::clone(&input.topic), input.partition))
2018 });
2019 if cut_is_paused {
2020 let boundary = KafkaDrainBoundary {
2021 round: active.request.round,
2022 inputs: Arc::clone(&active.inputs),
2023 };
2024 channel_len.fetch_add(1, Ordering::Relaxed);
2025 let sent = tokio::select! {
2026 biased;
2027 _ = reader_shutdown.changed() => false,
2028 () = tokio::time::sleep_until(active.prepare_deadline) => false,
2029 result = msg_tx.send(KafkaReaderItem::DrainBoundary(boundary)) => result.is_ok(),
2030 };
2031 if !sent {
2032 channel_len.fetch_sub(1, Ordering::Relaxed);
2033 if tokio::time::Instant::now() >= active.prepare_deadline {
2034 publish_reader_fault(
2035 &reader_fault,
2036 &data_ready,
2037 "Kafka drain boundary delivery exceeded its engine deadline",
2038 );
2039 } else if !*reader_shutdown.borrow() {
2040 publish_reader_fault(
2041 &reader_fault,
2042 &data_ready,
2043 "reader drain-boundary channel closed unexpectedly",
2044 );
2045 }
2046 break;
2047 }
2048 active.boundary_queued = true;
2049 data_ready.notify_one();
2050 }
2051 }
2052 }
2053
2054 let cur_assign_gen = assign_generation.load(Ordering::Acquire);
2059 if cur_assign_gen != last_assign_gen {
2060 let mut assigned: Vec<(String, i32)> = lock_or_recover(&rebalance_state)
2061 .assigned_partitions()
2062 .iter()
2063 .cloned()
2064 .collect();
2065 if assigned.is_empty() {
2066 if let Ok(a) = consumer.assignment() {
2069 assigned = a
2070 .elements()
2071 .iter()
2072 .map(|e| (e.topic().to_string(), e.partition()))
2073 .collect();
2074 }
2075 }
2076 let deterministic_fallback = if deterministic_unrecorded.load(Ordering::Acquire)
2077 {
2078 deterministic_default
2079 } else {
2080 None
2081 };
2082 let seek_tpl = match assignment_seek_tpl(
2083 &lock_or_recover(&reassign_snapshot),
2084 &assigned,
2085 if reassign_baselines.is_empty() {
2086 None
2087 } else {
2088 Some(&reassign_baselines)
2089 },
2090 deterministic_fallback,
2091 deterministic_unrecorded.load(Ordering::Acquire),
2092 ) {
2093 Ok(tpl) => tpl,
2094 Err(e) => {
2095 warn!(error = %e, "failed to build Kafka recovery assignment");
2096 publish_reader_fault(
2097 &reader_fault,
2098 &data_ready,
2099 format!("invalid recovery assignment: {e}"),
2100 );
2101 return;
2102 }
2103 };
2104 let seek_ok = if seek_tpl.count() == 0 {
2105 true } else {
2107 let seek_consumer = Arc::clone(&consumer);
2108 match blocking_tasks
2109 .run(move || {
2110 seek_consumer
2111 .seek_partitions(seek_tpl, std::time::Duration::from_secs(5))
2112 })
2113 .await
2114 {
2115 Ok(Ok(result))
2116 if assign_generation.load(Ordering::Acquire) == cur_assign_gen =>
2117 {
2118 let failed = result
2119 .elements()
2120 .iter()
2121 .filter(|e| e.error().is_err())
2122 .count();
2123 if failed == 0 {
2124 info!(
2125 partition_count = result.count(),
2126 "seeked assigned partitions to checkpointed offsets"
2127 );
2128 true
2129 } else {
2130 debug!(failed, "assign-seek incomplete; will retry");
2132 false
2133 }
2134 }
2135 Ok(Ok(_)) => false,
2136 Ok(Err(e)) => {
2137 debug!(error = %e, "assign-seek failed; will retry");
2138 false
2139 }
2140 Err(error) => {
2141 warn!(%error, "Kafka assign-seek worker failed");
2142 if error == KafkaBlockingTaskError::Retired {
2143 return;
2144 }
2145 false
2146 }
2147 }
2148 };
2149 if seek_ok {
2150 let mut resumed_ok = true;
2153 if !is_paused && active_drain.is_none() {
2154 if let Ok(assignment) = consumer.assignment() {
2155 if let Err(e) = consumer.resume(&assignment) {
2156 warn!(error = %e, "post-seek resume failed; will retry");
2157 resumed_ok = false;
2158 } else if let Err(error) = validate_kafka_partition_results(
2159 "post-seek resume",
2160 &assignment,
2161 ) {
2162 warn!(%error, "post-seek resume incomplete; will retry");
2163 resumed_ok = false;
2164 }
2165 }
2166 }
2167 if resumed_ok {
2168 last_assign_gen = cur_assign_gen;
2169 } else {
2170 continue;
2171 }
2172 }
2173 }
2174
2175 let fill = if reader_channel_capacity > 0 {
2177 let channel_len = u32::try_from(channel_len.load(Ordering::Acquire))
2178 .map_or(f64::from(u32::MAX), f64::from);
2179 channel_len / reader_channel_capacity_f64
2180 } else {
2181 0.0
2182 };
2183 if active_drain.is_none() && fill >= pause_threshold && !is_paused {
2184 if let Ok(assignment) = consumer.assignment() {
2185 match consumer.pause(&assignment) {
2186 Ok(()) => match validate_kafka_partition_results(
2187 "backpressure pause",
2188 &assignment,
2189 ) {
2190 Ok(()) => {
2191 is_paused = true;
2192 debug!("reader: paused Kafka partitions (fill={fill:.2})");
2193 }
2194 Err(error) => {
2195 warn!(%error, "reader backpressure pause incomplete");
2196 }
2197 },
2198 Err(error) => {
2199 warn!(%error, "reader backpressure pause failed");
2200 }
2201 }
2202 }
2203 } else if active_drain.is_none() && fill <= resume_threshold && is_paused {
2204 if let Ok(assignment) = consumer.assignment() {
2205 let resumed = consumer
2206 .resume(&assignment)
2207 .map_err(|error| format!("Kafka backpressure resume failed: {error}"))
2208 .and_then(|()| {
2209 validate_kafka_partition_results("backpressure resume", &assignment)
2210 });
2211 if let Err(error) = resumed {
2212 warn!(%error, "reader backpressure resume incomplete");
2213 continue;
2214 }
2215 is_paused = false;
2216 debug!("reader: resumed Kafka partitions (fill={fill:.2})");
2217 }
2218 }
2219
2220 let recv_timeout = if is_paused {
2225 std::time::Duration::from_millis(10)
2226 } else {
2227 std::time::Duration::from_millis(200)
2228 };
2229 let drain_held = active_drain
2230 .as_ref()
2231 .is_some_and(|drain| drain.boundary_queued);
2232 let prepare_deadline = active_drain
2233 .as_ref()
2234 .filter(|drain| !drain.boundary_queued)
2235 .map(|drain| drain.prepare_deadline);
2236 let resolution_deferred = deferred_drain_command.is_some();
2237 let msg_result = tokio::select! {
2238 biased;
2239 _ = reader_shutdown.changed() => break,
2240 () = async {
2241 match prepare_deadline {
2242 Some(deadline) => tokio::time::sleep_until(deadline).await,
2243 None => std::future::pending().await,
2244 }
2245 } => {
2246 publish_reader_fault(
2247 &reader_fault,
2248 &data_ready,
2249 "Kafka source drain preparation exceeded its engine deadline",
2250 );
2251 return;
2252 },
2253 command = async {
2254 match reader_drain_rx.as_mut() {
2255 Some(receiver) => receiver.recv().await,
2256 None => std::future::pending().await,
2257 }
2258 }, if !resolution_deferred => {
2259 deferred_drain_command = command;
2260 if deferred_drain_command.is_none() {
2261 reader_drain_rx = None;
2262 }
2263 continue;
2264 },
2265 () = tokio::time::sleep(std::time::Duration::from_millis(10)), if drain_held => continue,
2266 msg = tokio::time::timeout(recv_timeout, consumer.recv()), if !drain_held => match msg {
2267 Ok(result) => result,
2268 Err(_timeout) => continue,
2269 },
2270 };
2271 match msg_result {
2272 Ok(msg) => {
2273 if let Some(payload) = msg.payload() {
2274 let topic = msg.topic();
2275 if &*cached_topic != topic {
2276 if vnode_reassign.is_some() {
2277 let Some((canonical_topic, routes)) =
2278 vnode_partition_routes.get_key_value(topic)
2279 else {
2280 warn!(
2281 topic,
2282 "Kafka reader received a topic outside its activated vnode inventory"
2283 );
2284 publish_reader_fault(
2285 &reader_fault,
2286 &data_ready,
2287 "payload topic is outside the activated inventory",
2288 );
2289 return;
2290 };
2291 cached_topic = Arc::clone(canonical_topic);
2292 cached_topic_routes = Some(Arc::clone(routes));
2293 } else {
2294 cached_topic = Arc::from(topic);
2295 cached_topic_routes = None;
2296 }
2297 }
2298 let partition_vnode = match cached_partition_vnode(
2299 cached_topic_routes.as_deref(),
2300 msg.partition(),
2301 ) {
2302 Ok(vnode) => vnode,
2303 Err(error) => {
2304 warn!(
2305 topic,
2306 partition = msg.partition(),
2307 %error,
2308 "Kafka reader rejected a payload outside its activated vnode inventory"
2309 );
2310 publish_reader_fault(
2311 &reader_fault,
2312 &data_ready,
2313 format!("payload route is outside the activated inventory: {error}"),
2314 );
2315 return;
2316 }
2317 };
2318 let timestamp_ms = match msg.timestamp() {
2319 rdkafka::Timestamp::CreateTime(ts)
2320 | rdkafka::Timestamp::LogAppendTime(ts) => Some(ts),
2321 rdkafka::Timestamp::NotAvailable => None,
2322 };
2323 let headers_json = if capture_headers {
2324 use rdkafka::message::Headers;
2325 msg.headers().and_then(|hdrs| {
2326 let pairs: Vec<(String, serde_json::Value)> = (0..hdrs.count())
2327 .map(|i| {
2328 let h = hdrs.get(i);
2329 let val = match h.value {
2330 Some(v) => serde_json::Value::String(
2331 String::from_utf8_lossy(v).into_owned(),
2332 ),
2333 None => serde_json::Value::Null,
2334 };
2335 (h.key.to_string(), val)
2336 })
2337 .collect();
2338 serde_json::to_string(&pairs).ok()
2339 })
2340 } else {
2341 None
2342 };
2343 let kp = KafkaPayload {
2344 data: payload.to_vec(),
2345 topic: Arc::clone(&cached_topic),
2346 partition: msg.partition(),
2347 partition_vnode,
2348 offset: msg.offset(),
2349 timestamp_ms,
2350 headers_json,
2351 };
2352 let item = KafkaReaderItem::Payload(kp);
2353 match msg_tx.try_send(item) {
2354 Ok(()) => {
2355 channel_len.fetch_add(1, Ordering::Relaxed);
2356 }
2357 Err(crossfire::TrySendError::Full(item)) => {
2358 if !is_paused {
2359 if let Ok(assignment) = consumer.assignment() {
2360 let paused = consumer
2361 .pause(&assignment)
2362 .map_err(|error| {
2363 format!(
2364 "Kafka full-channel pause failed: {error}"
2365 )
2366 })
2367 .and_then(|()| {
2368 validate_kafka_partition_results(
2369 "full-channel pause",
2370 &assignment,
2371 )
2372 });
2373 match paused {
2374 Ok(()) => {
2375 is_paused = true;
2376 debug!(
2377 "reader: paused partitions (channel full)"
2378 );
2379 }
2380 Err(error) => {
2381 warn!(%error, "reader full-channel pause incomplete");
2382 }
2383 }
2384 }
2385 }
2386 channel_len.fetch_add(1, Ordering::Relaxed);
2387 let send_ok = tokio::select! {
2388 biased;
2389 _ = reader_shutdown.changed() => false,
2390 result = msg_tx.send(item) => result.is_ok(),
2391 };
2392 if !send_ok {
2393 channel_len.fetch_sub(1, Ordering::Relaxed);
2394 if !*reader_shutdown.borrow() {
2395 publish_reader_fault(
2396 &reader_fault,
2397 &data_ready,
2398 "reader output channel closed unexpectedly",
2399 );
2400 }
2401 break;
2402 }
2403 }
2404 Err(crossfire::TrySendError::Disconnected(_)) => {
2405 if !*reader_shutdown.borrow() {
2406 publish_reader_fault(
2407 &reader_fault,
2408 &data_ready,
2409 "reader output channel disconnected unexpectedly",
2410 );
2411 }
2412 break;
2413 }
2414 }
2415 data_ready.notify_one();
2416 }
2417 }
2418 Err(e) if kafka_reader_error_is_transient(&e) => {
2419 debug!(error = %e, "Kafka consumer poll event");
2420 }
2421 Err(e) => {
2422 warn!(error = %e, "Kafka consumer error");
2423 publish_reader_fault(
2424 &reader_fault,
2425 &data_ready,
2426 format!("terminal Kafka consumer error: {e}"),
2427 );
2428 break;
2429 }
2430 }
2431 }
2432
2433 data_ready.notify_one();
2438 });
2439
2440 self.msg_rx = Some(msg_rx);
2441 self.reader_handle = Some(reader_handle);
2442 self.reader_shutdown = Some(shutdown_tx);
2443 self.reader_drain_tx = reader_drain_tx;
2444 }
2445
2446 fn capture_vnode_checkpoint(
2447 &self,
2448 publication: &KafkaAssignmentPublication,
2449 ) -> Result<SourceCheckpoint, ConnectorError> {
2450 let mut checkpoint = self.offsets.to_checkpoint_for_partitions(
2451 publication
2452 .owned_partitions
2453 .iter()
2454 .filter(|(topic, partition)| {
2455 rotation_partition_baseline(&publication.baselines, topic.as_str(), *partition)
2456 .is_none()
2457 })
2458 .map(|(topic, partition)| (topic.as_str(), *partition)),
2459 );
2460 attach_partition_baselines(
2461 &mut checkpoint,
2462 &self.manual_partition_baselines,
2463 &publication.owned_partitions,
2464 );
2465 attach_rotation_baselines(
2466 &mut checkpoint,
2467 &publication.baselines,
2468 &publication.owned_partitions,
2469 );
2470 let assignment_version =
2471 NonZeroU64::new(publication.assignment_version).ok_or_else(|| {
2472 ConnectorError::InvalidState {
2473 expected: "a positive vnode assignment version".into(),
2474 actual: publication.assignment_version.to_string(),
2475 }
2476 })?;
2477 checkpoint.bind_assignment_version(assignment_version);
2478 Ok(checkpoint)
2479 }
2480
2481 fn capture_non_vnode_checkpoint(&self) -> SourceCheckpoint {
2482 if !self.manual_topic_partitions.is_empty() {
2483 let mut checkpoint = self.offsets.to_checkpoint_for_partitions(
2484 self.manual_topic_partitions
2485 .iter()
2486 .map(|(topic, partition)| (topic.as_str(), *partition)),
2487 );
2488 attach_partition_baselines(
2489 &mut checkpoint,
2490 &self.manual_partition_baselines,
2491 &self.manual_topic_partitions,
2492 );
2493 checkpoint.set_metadata(
2494 KAFKA_PARTITION_INVENTORY_METADATA,
2495 encode_partition_inventory(&self.manual_topic_partitions),
2496 );
2497 return checkpoint;
2498 }
2499 let assigned = lock_or_recover(&self.rebalance_state).assignment_snapshot();
2500 self.offsets.to_checkpoint_for_partitions(
2501 assigned
2502 .iter()
2503 .map(|(topic, partition)| (topic.as_str(), *partition)),
2504 )
2505 }
2506
2507 fn try_capture_checkpoint(&self) -> Result<Option<SourceCheckpoint>, ConnectorError> {
2508 self.check_reader_health("capturing a checkpoint cursor")?;
2509 let Some((registry, _)) = &self.vnode_assignment else {
2510 return Ok(Some(self.capture_non_vnode_checkpoint()));
2511 };
2512 try_capture_at_assignment_fence(
2515 registry,
2516 &self.reconciled_assignment_version,
2517 &self.assignment_publication,
2518 |publication| self.capture_vnode_checkpoint(publication),
2519 )
2520 }
2521}
2522
2523fn tpl_of<'a>(parts: impl Iterator<Item = &'a (Arc<str>, i32)>) -> TopicPartitionList {
2526 let mut tpl = TopicPartitionList::new();
2527 for (topic, partition) in parts {
2528 let _ = tpl.add_partition(topic.as_ref(), *partition);
2529 }
2530 tpl
2531}
2532
2533#[derive(Clone, Copy)]
2537struct VnodeResumeCursors<'a> {
2538 local_offsets: &'a OffsetTracker,
2539 handoff_offsets: &'a OffsetTracker,
2540 local_baselines: &'a KafkaPartitionBaselines,
2541 handoff_baselines: &'a KafkaPartitionBaselines,
2542}
2543
2544fn build_vnode_assignment_tpl(
2545 source_identity: &str,
2546 assignment: &[laminar_core::state::NodeId],
2547 self_id: laminar_core::state::NodeId,
2548 topic_meta: &[(Arc<str>, i32)],
2549 cursors: VnodeResumeCursors<'_>,
2550 default_offset: rdkafka::Offset,
2551) -> Result<TopicPartitionList, ConnectorError> {
2552 let mut tpl = TopicPartitionList::new();
2553 for (topic, count) in topic_meta {
2554 for partition in super::vnode_routing::owned_partitions_in_assignment(
2555 source_identity,
2556 topic.as_ref(),
2557 *count,
2558 assignment,
2559 self_id,
2560 )? {
2561 let offset = match cursors
2562 .local_offsets
2563 .get(topic.as_ref(), partition)
2564 .or_else(|| cursors.handoff_offsets.get(topic.as_ref(), partition))
2567 {
2568 Some(offset) => {
2569 rdkafka::Offset::Offset(offset.checked_add(1).ok_or_else(|| {
2570 ConnectorError::ConfigurationError(format!(
2571 "Kafka checkpoint offset overflow for '{topic}-{partition}'"
2572 ))
2573 })?)
2574 }
2575 None => cursors
2576 .handoff_baselines
2577 .get(&(topic.to_string(), partition))
2578 .or_else(|| cursors.local_baselines.get(&(topic.to_string(), partition)))
2579 .map_or(default_offset, |next| rdkafka::Offset::Offset(*next)),
2580 };
2581 tpl.add_partition_offset(topic.as_ref(), partition, offset)
2582 .map_err(|error| {
2583 ConnectorError::Internal(format!(
2584 "failed to add vnode-owned Kafka partition '{topic}-{partition}' to assignment: {error}"
2585 ))
2586 })?;
2587 }
2588 }
2589 Ok(tpl)
2590}
2591
2592fn acquired_numeric_position(
2596 handoff: &OffsetTracker,
2597 handoff_baselines: &KafkaPartitionBaselines,
2598 local: &OffsetTracker,
2599 local_baselines: &KafkaPartitionBaselines,
2600 topic: &str,
2601 partition: i32,
2602 allow_local: bool,
2603) -> Result<Option<i64>, ConnectorError> {
2604 if let Some(offset) = handoff.get(topic, partition) {
2605 return offset.checked_add(1).map(Some).ok_or_else(|| {
2606 ConnectorError::ConfigurationError(format!(
2607 "Kafka handoff offset overflow for '{topic}-{partition}'"
2608 ))
2609 });
2610 }
2611 if let Some(next) = handoff_baselines
2612 .get(&(topic.to_string(), partition))
2613 .copied()
2614 {
2615 return Ok(Some(next));
2616 }
2617 if allow_local {
2618 if let Some(offset) = local.get(topic, partition) {
2619 return offset.checked_add(1).map(Some).ok_or_else(|| {
2620 ConnectorError::ConfigurationError(format!(
2621 "Kafka local offset overflow for '{topic}-{partition}'"
2622 ))
2623 });
2624 }
2625 if let Some(next) = local_baselines.get(&(topic.to_string(), partition)) {
2626 return Ok(Some(*next));
2627 }
2628 }
2629 Ok(None)
2630}
2631
2632fn assignment_seek_tpl(
2637 offsets: &OffsetTracker,
2638 assigned: &[(String, i32)],
2639 baselines: Option<&KafkaPartitionBaselines>,
2640 deterministic_fallback: Option<rdkafka::Offset>,
2641 require_all: bool,
2642) -> Result<TopicPartitionList, ConnectorError> {
2643 let mut tpl = TopicPartitionList::new();
2644 for (topic, partition) in assigned {
2645 let position = match offsets.get(topic, *partition) {
2646 Some(offset) => rdkafka::Offset::Offset(offset.checked_add(1).ok_or_else(|| {
2647 ConnectorError::ConfigurationError(format!(
2648 "Kafka checkpoint offset overflow for '{topic}-{partition}'"
2649 ))
2650 })?),
2651 None => {
2652 if let Some(next) =
2653 baselines.and_then(|positions| positions.get(&(topic.clone(), *partition)))
2654 {
2655 rdkafka::Offset::Offset(*next)
2656 } else if let Some(offset) = deterministic_fallback {
2657 offset
2658 } else if require_all {
2659 return Err(ConnectorError::ConfigurationError(format!(
2660 "Kafka partition '{topic}-{partition}' has no durable next-to-read baseline"
2661 )));
2662 } else {
2663 continue;
2664 }
2665 }
2666 };
2667 tpl.add_partition_offset(topic, *partition, position)
2668 .map_err(|e| {
2669 ConnectorError::Internal(format!(
2670 "failed to build Kafka assignment seek for '{topic}-{partition}': {e}"
2671 ))
2672 })?;
2673 }
2674 Ok(tpl)
2675}
2676
2677fn startup_default_offset(mode: &StartupMode) -> rdkafka::Offset {
2680 match mode {
2681 StartupMode::Earliest => rdkafka::Offset::Beginning,
2682 StartupMode::Latest => rdkafka::Offset::End,
2683 _ => rdkafka::Offset::Stored,
2687 }
2688}
2689
2690fn deterministic_initial_offset(mode: &StartupMode, reset: OffsetReset) -> Option<rdkafka::Offset> {
2695 match (mode, reset) {
2696 (StartupMode::Latest, _) | (StartupMode::GroupOffsets, OffsetReset::Latest) => {
2697 Some(rdkafka::Offset::End)
2698 }
2699 (StartupMode::GroupOffsets, OffsetReset::None)
2700 | (StartupMode::SpecificOffsets(_) | StartupMode::Timestamp(_), _) => None,
2701 (StartupMode::GroupOffsets, OffsetReset::Earliest) | (StartupMode::Earliest, _) => {
2702 Some(rdkafka::Offset::Beginning)
2703 }
2704 }
2705}
2706
2707fn encode_partition_inventory(inventory: &KafkaPartitionSet) -> String {
2708 let mut canonical: Vec<_> = inventory.iter().cloned().collect();
2709 canonical.sort_unstable();
2710 serde_json::to_string(&canonical).expect("Kafka partition inventory is serializable")
2711}
2712
2713fn decode_partition_inventory(
2714 checkpoint: &SourceCheckpoint,
2715) -> Result<Option<KafkaPartitionSet>, ConnectorError> {
2716 let Some(encoded) = checkpoint.get_metadata(KAFKA_PARTITION_INVENTORY_METADATA) else {
2717 return Ok(None);
2718 };
2719 let canonical: Vec<(String, i32)> = serde_json::from_str(encoded).map_err(|error| {
2720 ConnectorError::ConfigurationError(format!(
2721 "invalid Kafka checkpoint partition inventory: {error}"
2722 ))
2723 })?;
2724 if canonical
2725 .iter()
2726 .any(|(topic, partition)| topic.is_empty() || *partition < 0)
2727 || canonical.windows(2).any(|pair| pair[0] >= pair[1])
2728 {
2729 return Err(ConnectorError::ConfigurationError(
2730 "invalid Kafka checkpoint partition inventory: entries must be canonical, unique, and non-negative"
2731 .into(),
2732 ));
2733 }
2734 Ok(Some(canonical.into_iter().collect()))
2735}
2736
2737fn validate_resume_inventory(
2738 checkpoint: Option<&KafkaPartitionSet>,
2739 current: &KafkaPartitionSet,
2740) -> Result<(), ConnectorError> {
2741 let checkpoint = checkpoint.ok_or_else(|| {
2742 ConnectorError::ConfigurationError(
2743 "Kafka engine-owned resume checkpoint has no partition inventory".into(),
2744 )
2745 })?;
2746 if checkpoint != current {
2747 return Err(ConnectorError::ConfigurationError(format!(
2748 "Kafka partition inventory changed across recovery: checkpoint={checkpoint:?}, current={current:?}"
2749 )));
2750 }
2751 Ok(())
2752}
2753
2754fn partition_baseline_key(topic: &str, partition: i32) -> String {
2755 format!("{KAFKA_PARTITION_BASELINE_PREFIX}{topic}:{partition}")
2756}
2757
2758fn decode_partition_baselines_from_offsets(
2759 offsets: &std::collections::HashMap<String, String>,
2760) -> Result<KafkaPartitionBaselines, ConnectorError> {
2761 let mut baselines = KafkaPartitionBaselines::new();
2762 for (key, value) in offsets {
2763 let Some(encoded) = key.strip_prefix(KAFKA_PARTITION_BASELINE_PREFIX) else {
2764 continue;
2765 };
2766 let (topic, partition_text) = encoded.rsplit_once(':').ok_or_else(|| {
2767 ConnectorError::ConfigurationError(format!(
2768 "invalid Kafka partition baseline key '{key}'"
2769 ))
2770 })?;
2771 let partition = partition_text.parse::<i32>().map_err(|_| {
2772 ConnectorError::ConfigurationError(format!(
2773 "invalid Kafka partition baseline key '{key}'"
2774 ))
2775 })?;
2776 let next = value.parse::<i64>().map_err(|_| {
2777 ConnectorError::ConfigurationError(format!(
2778 "invalid Kafka next-to-read baseline for '{topic}-{partition_text}': '{value}'"
2779 ))
2780 })?;
2781 if topic.is_empty()
2782 || topic.contains(':')
2783 || partition < 0
2784 || partition.to_string() != partition_text
2785 || next < 0
2786 || next == i64::MAX
2787 || next.to_string() != value.as_str()
2788 {
2789 return Err(ConnectorError::ConfigurationError(format!(
2790 "invalid Kafka partition baseline '{key}' = '{value}'"
2791 )));
2792 }
2793 if baselines
2794 .insert((topic.to_string(), partition), next)
2795 .is_some()
2796 {
2797 return Err(ConnectorError::ConfigurationError(format!(
2798 "duplicate Kafka partition baseline for '{topic}-{partition}'"
2799 )));
2800 }
2801 }
2802 Ok(baselines)
2803}
2804
2805fn decode_partition_baselines(
2806 checkpoint: &SourceCheckpoint,
2807) -> Result<KafkaPartitionBaselines, ConnectorError> {
2808 decode_partition_baselines_from_offsets(checkpoint.offsets())
2809}
2810
2811fn attach_partition_baselines(
2812 checkpoint: &mut SourceCheckpoint,
2813 baselines: &KafkaPartitionBaselines,
2814 included: &KafkaPartitionSet,
2815) {
2816 for ((topic, partition), next) in baselines {
2817 if included.contains(&(topic.clone(), *partition)) {
2818 checkpoint.set_offset(partition_baseline_key(topic, *partition), next.to_string());
2819 }
2820 }
2821}
2822
2823fn rotation_partition_baseline(
2824 baselines: &KafkaRotationBaselines,
2825 topic: &str,
2826 partition: i32,
2827) -> Option<i64> {
2828 baselines
2829 .get(topic)
2830 .and_then(|partitions| partitions.get(&partition))
2831 .copied()
2832}
2833
2834fn rotation_baselines_len(baselines: &KafkaRotationBaselines) -> usize {
2835 baselines.values().map(std::collections::HashMap::len).sum()
2836}
2837
2838fn update_rotation_baselines(
2839 current: &KafkaRotationBaselines,
2840 owned: &KafkaPartitionSet,
2841 acquired: &KafkaPartitionBaselines,
2842) -> KafkaRotationBaselines {
2843 let mut updated = KafkaRotationBaselines::new();
2844 for (topic, partition) in owned {
2845 if let Some(next) = rotation_partition_baseline(current, topic, *partition) {
2846 updated
2847 .entry(Arc::from(topic.as_str()))
2848 .or_default()
2849 .insert(*partition, next);
2850 }
2851 }
2852 for ((topic, partition), next) in acquired {
2853 updated
2854 .entry(Arc::from(topic.as_str()))
2855 .or_default()
2856 .insert(*partition, *next);
2857 }
2858 updated
2859}
2860
2861fn attach_rotation_baselines(
2862 checkpoint: &mut SourceCheckpoint,
2863 baselines: &KafkaRotationBaselines,
2864 included: &KafkaPartitionSet,
2865) {
2866 for (topic, partition) in included {
2867 if let Some(next) = rotation_partition_baseline(baselines, topic, *partition) {
2868 checkpoint.set_offset(partition_baseline_key(topic, *partition), next.to_string());
2869 }
2870 }
2871}
2872
2873fn vnode_payload_is_current(
2874 ownership: Option<(&[laminar_core::state::NodeId], laminar_core::state::NodeId)>,
2875 partition_vnode: Option<u32>,
2876 required_next: Option<i64>,
2877 offset: i64,
2878) -> Result<bool, ConnectorError> {
2879 let owned = if let Some((assignment, self_id)) = ownership {
2880 let vnode = partition_vnode.ok_or_else(|| {
2881 ConnectorError::ConfigurationError(
2882 "Kafka payload has no canonical source/topic/partition vnode route".into(),
2883 )
2884 })?;
2885 let vnode_index = usize::try_from(vnode).map_err(|_| {
2886 ConnectorError::ConfigurationError(
2887 "Kafka vnode id cannot be represented on this platform".into(),
2888 )
2889 })?;
2890 let owner = assignment.get(vnode_index).ok_or_else(|| {
2891 ConnectorError::ConfigurationError(format!(
2892 "Kafka cached vnode {vnode} is outside owner map cardinality {}",
2893 assignment.len()
2894 ))
2895 })?;
2896 *owner == self_id
2897 } else {
2898 true
2899 };
2900 Ok(owned && required_next.is_none_or(|next| offset >= next))
2901}
2902
2903fn retire_accepted_rotation_baselines(
2904 baselines: &mut KafkaRotationBaselines,
2905 accepted_offsets: &[(Arc<str>, i32, i64)],
2906) {
2907 let mut accepted = std::collections::HashMap::<(&str, i32), i64>::new();
2908 for (topic, partition, offset) in accepted_offsets {
2909 accepted
2910 .entry((topic.as_ref(), *partition))
2911 .and_modify(|current| *current = (*current).max(*offset))
2912 .or_insert(*offset);
2913 }
2914 baselines.retain(|topic, partitions| {
2915 partitions.retain(|partition, next| {
2916 accepted
2917 .get(&(topic.as_ref(), *partition))
2918 .is_none_or(|offset| offset < next)
2919 });
2920 !partitions.is_empty()
2921 });
2922}
2923
2924fn validate_partition_baselines(
2925 baselines: &KafkaPartitionBaselines,
2926 inventory: &KafkaPartitionSet,
2927) -> Result<(), ConnectorError> {
2928 let baseline_inventory: KafkaPartitionSet = baselines.keys().cloned().collect();
2929 if baseline_inventory != *inventory {
2930 return Err(ConnectorError::ConfigurationError(format!(
2931 "Kafka guaranteed recovery baseline inventory does not match its partition cut: baselines={baseline_inventory:?}, partitions={inventory:?}"
2932 )));
2933 }
2934 Ok(())
2935}
2936
2937fn validate_positions_not_expired(
2938 offsets: &OffsetTracker,
2939 baselines: &KafkaPartitionBaselines,
2940 low_watermarks: &KafkaPartitionBaselines,
2941 inventory: &KafkaPartitionSet,
2942) -> Result<(), ConnectorError> {
2943 for (topic, partition) in inventory {
2944 let desired = match offsets.get(topic, *partition) {
2945 Some(last) => last.checked_add(1).ok_or_else(|| {
2946 ConnectorError::ConfigurationError(format!(
2947 "Kafka checkpoint offset overflow for '{topic}-{partition}'"
2948 ))
2949 })?,
2950 None => *baselines.get(&(topic.clone(), *partition)).ok_or_else(|| {
2951 ConnectorError::ConfigurationError(format!(
2952 "Kafka partition '{topic}-{partition}' has no durable next-to-read baseline"
2953 ))
2954 })?,
2955 };
2956 let low = *low_watermarks
2957 .get(&(topic.clone(), *partition))
2958 .ok_or_else(|| {
2959 ConnectorError::ConnectionFailed(format!(
2960 "Kafka watermark response omitted partition '{topic}-{partition}'"
2961 ))
2962 })?;
2963 if desired < low {
2964 return Err(ConnectorError::ConfigurationError(format!(
2965 "Kafka retention advanced partition '{topic}-{partition}' to {low} past the durable next-to-read position {desired}"
2966 )));
2967 }
2968 }
2969 Ok(())
2970}
2971
2972fn kafka_reader_error_is_transient(error: &KafkaError) -> bool {
2973 use rdkafka::types::RDKafkaErrorCode;
2974
2975 let code = match error {
2976 KafkaError::PartitionEOF(_) | KafkaError::NoMessageReceived => return true,
2977 KafkaError::MessageConsumption(code) | KafkaError::Global(code) => *code,
2978 _ => return false,
2979 };
2980
2981 matches!(
2982 code,
2983 RDKafkaErrorCode::BrokerDestroy
2984 | RDKafkaErrorCode::BrokerTransportFailure
2985 | RDKafkaErrorCode::Resolve
2986 | RDKafkaErrorCode::AllBrokersDown
2987 | RDKafkaErrorCode::OperationTimedOut
2988 | RDKafkaErrorCode::QueueFull
2989 | RDKafkaErrorCode::NodeUpdate
2990 | RDKafkaErrorCode::WaitingForCoordinator
2991 | RDKafkaErrorCode::UnknownGroup
2992 | RDKafkaErrorCode::InProgress
2993 | RDKafkaErrorCode::PreviousInProgress
2994 | RDKafkaErrorCode::TimedOutQueue
2995 | RDKafkaErrorCode::WaitCache
2996 | RDKafkaErrorCode::Interrupted
2997 | RDKafkaErrorCode::Partial
2998 | RDKafkaErrorCode::Retry
2999 | RDKafkaErrorCode::PollExceeded
3000 | RDKafkaErrorCode::UnknownBroker
3001 | RDKafkaErrorCode::AssignmentLost
3002 | RDKafkaErrorCode::DestroyBroker
3003 | RDKafkaErrorCode::UnknownTopicOrPartition
3004 | RDKafkaErrorCode::LeaderNotAvailable
3005 | RDKafkaErrorCode::NotLeaderForPartition
3006 | RDKafkaErrorCode::RequestTimedOut
3007 | RDKafkaErrorCode::BrokerNotAvailable
3008 | RDKafkaErrorCode::ReplicaNotAvailable
3009 | RDKafkaErrorCode::NetworkException
3010 | RDKafkaErrorCode::CoordinatorLoadInProgress
3011 | RDKafkaErrorCode::CoordinatorNotAvailable
3012 | RDKafkaErrorCode::NotCoordinator
3013 | RDKafkaErrorCode::IllegalGeneration
3014 | RDKafkaErrorCode::UnknownMemberId
3015 | RDKafkaErrorCode::RebalanceInProgress
3016 | RDKafkaErrorCode::NotController
3017 | RDKafkaErrorCode::KafkaStorageError
3018 | RDKafkaErrorCode::ReassignmentInProgress
3019 | RDKafkaErrorCode::FetchSessionIdNotFound
3020 | RDKafkaErrorCode::InvalidFetchSessionEpoch
3021 | RDKafkaErrorCode::FencedLeaderEpoch
3022 | RDKafkaErrorCode::UnknownLeaderEpoch
3023 | RDKafkaErrorCode::StaleBrokerEpoch
3024 | RDKafkaErrorCode::OffsetNotAvailable
3025 | RDKafkaErrorCode::MemberIdRequired
3026 | RDKafkaErrorCode::PreferredLeaderNotAvailable
3027 | RDKafkaErrorCode::EligibleLeadersNotAvailable
3028 | RDKafkaErrorCode::UnstableOffsetCommit
3029 | RDKafkaErrorCode::ThrottlingQuotaExceeded
3030 | RDKafkaErrorCode::UnknownTopicId
3031 )
3032}
3033
3034fn consumer_creation_error(error: &KafkaError) -> ConnectorError {
3035 ConnectorError::ConfigurationError(format!(
3036 "failed to create Kafka consumer from local configuration: {error}"
3037 ))
3038}
3039
3040async fn fetch_explicit_topic_metadata(
3041 blocking_tasks: KafkaBlockingTasks,
3042 consumer: Arc<StreamConsumer<LaminarConsumerContext>>,
3043 topics: Vec<String>,
3044) -> Result<Vec<(Arc<str>, i32)>, ConnectorError> {
3045 const METADATA_BUDGET: std::time::Duration = std::time::Duration::from_secs(10);
3046 let deadline = tokio::time::Instant::now() + METADATA_BUDGET;
3047 let task = blocking_tasks.run(move || {
3048 let blocking_deadline = std::time::Instant::now() + METADATA_BUDGET;
3049 let mut topic_meta = Vec::with_capacity(topics.len());
3050 for topic in topics {
3051 let remaining = blocking_deadline.saturating_duration_since(std::time::Instant::now());
3052 if remaining.is_zero() {
3053 return Err(ConnectorError::Timeout(
3054 u64::try_from(METADATA_BUDGET.as_millis()).unwrap_or(u64::MAX),
3055 ));
3056 }
3057 let metadata = consumer
3058 .fetch_metadata(Some(topic.as_str()), remaining)
3059 .map_err(|error| fetch_error(&topic, &error))?;
3060 let topic_metadata = metadata
3061 .topics()
3062 .iter()
3063 .find(|candidate| candidate.name() == topic.as_str())
3064 .ok_or_else(|| invalid_response(&topic, "metadata response omitted the topic"))?;
3065 if let Some(error) = topic_metadata.error() {
3066 return Err(topic_error(&topic, error.into()));
3067 }
3068 let partition_count = topic_metadata.partitions().len();
3069 if partition_count == 0 {
3070 return Err(invalid_response(&topic, "broker returned no partitions"));
3071 }
3072 topic_meta.push((
3073 Arc::from(topic.as_str()),
3074 i32::try_from(partition_count).unwrap_or(i32::MAX),
3075 ));
3076 }
3077 Ok(topic_meta)
3078 });
3079 match tokio::time::timeout_at(deadline, task).await {
3080 Ok(Ok(result)) => result,
3081 Ok(Err(error)) => Err(ConnectorError::Internal(format!(
3082 "Kafka metadata worker failed: {error}"
3083 ))),
3084 Err(_) => Err(ConnectorError::Timeout(
3085 u64::try_from(METADATA_BUDGET.as_millis()).unwrap_or(u64::MAX),
3086 )),
3087 }
3088}
3089
3090async fn fetch_partition_low_watermarks(
3091 blocking_tasks: KafkaBlockingTasks,
3092 consumer: Arc<StreamConsumer<LaminarConsumerContext>>,
3093 partitions: &KafkaPartitionSet,
3094) -> Result<KafkaPartitionBaselines, ConnectorError> {
3095 let deadline = tokio::time::Instant::now() + KAFKA_POSITION_LOOKUP_BUDGET;
3096 let mut remaining = partitions.iter().cloned();
3097 let mut jobs = tokio::task::JoinSet::new();
3098 let mut baselines = KafkaPartitionBaselines::with_capacity(partitions.len());
3099
3100 loop {
3101 while jobs.len() < KAFKA_POSITION_LOOKUP_CONCURRENCY {
3102 let Some((topic, partition)) = remaining.next() else {
3103 break;
3104 };
3105 jobs.spawn(fetch_partition_low_watermark(
3106 blocking_tasks.clone(),
3107 Arc::clone(&consumer),
3108 topic,
3109 partition,
3110 deadline,
3111 ));
3112 }
3113 let Some(result) = jobs.join_next().await else {
3114 break;
3115 };
3116 let ((topic, partition), low) = result.map_err(|error| {
3117 ConnectorError::Internal(format!("Kafka watermark worker failed: {error}"))
3118 })??;
3119 baselines.insert((topic, partition), low);
3120 }
3121
3122 Ok(baselines)
3123}
3124
3125async fn fetch_partition_low_watermark(
3126 blocking_tasks: KafkaBlockingTasks,
3127 consumer: Arc<StreamConsumer<LaminarConsumerContext>>,
3128 topic: String,
3129 partition: i32,
3130 deadline: tokio::time::Instant,
3131) -> Result<((String, i32), i64), ConnectorError> {
3132 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
3133 if remaining.is_zero() {
3134 return Err(ConnectorError::Timeout(
3135 u64::try_from(KAFKA_POSITION_LOOKUP_BUDGET.as_millis()).unwrap_or(u64::MAX),
3136 ));
3137 }
3138 let task_topic = topic.clone();
3139 let task = blocking_tasks.run(move || {
3140 consumer
3141 .fetch_watermarks(&task_topic, partition, remaining)
3142 .map_err(|error| {
3143 ConnectorError::ConnectionFailed(format!(
3144 "failed to fetch Kafka watermarks for '{task_topic}-{partition}': {error}"
3145 ))
3146 })
3147 });
3148 match tokio::time::timeout_at(deadline, task).await {
3149 Ok(Ok(Ok((low, _high)))) if (0..i64::MAX).contains(&low) => Ok(((topic, partition), low)),
3150 Ok(Ok(Ok((low, _)))) => Err(ConnectorError::ConnectionFailed(format!(
3151 "Kafka returned invalid low watermark {low} for '{topic}-{partition}'"
3152 ))),
3153 Ok(Ok(Err(error))) => Err(error),
3154 Ok(Err(error)) => Err(ConnectorError::Internal(format!(
3155 "Kafka watermark lookup task failed: {error}"
3156 ))),
3157 Err(_) => Err(ConnectorError::Timeout(
3158 u64::try_from(KAFKA_POSITION_LOOKUP_BUDGET.as_millis()).unwrap_or(u64::MAX),
3159 )),
3160 }
3161}
3162
3163async fn resolve_timestamp_offsets(
3164 blocking_tasks: KafkaBlockingTasks,
3165 consumer: Arc<StreamConsumer<LaminarConsumerContext>>,
3166 requested: TopicPartitionList,
3167) -> Result<TopicPartitionList, ConnectorError> {
3168 const LOOKUP_BUDGET: std::time::Duration = std::time::Duration::from_secs(10);
3169 let deadline = tokio::time::Instant::now() + LOOKUP_BUDGET;
3170 let task = blocking_tasks.run(move || {
3171 consumer
3172 .offsets_for_times(requested, LOOKUP_BUDGET)
3173 .map_err(|error| {
3174 ConnectorError::ConnectionFailed(format!(
3175 "failed to resolve Kafka timestamp offsets: {error}"
3176 ))
3177 })
3178 });
3179 match tokio::time::timeout_at(deadline, task).await {
3180 Ok(Ok(result)) => result,
3181 Ok(Err(error)) => Err(ConnectorError::Internal(format!(
3182 "Kafka timestamp worker failed: {error}"
3183 ))),
3184 Err(_) => Err(ConnectorError::Timeout(
3185 u64::try_from(LOOKUP_BUDGET.as_millis()).unwrap_or(u64::MAX),
3186 )),
3187 }
3188}
3189
3190impl KafkaSource {
3191 async fn decode_polled_payloads(
3192 &mut self,
3193 ) -> Result<(arrow_array::RecordBatch, Option<Vec<usize>>), ConnectorError> {
3194 if let Some(avro_deser) = self
3197 .deserializer
3198 .as_any_mut()
3199 .and_then(|any| any.downcast_mut::<AvroDeserializer>())
3200 {
3201 let mut new_schema_ids = Vec::new();
3202 for &(start, len) in &self.poll_payload_offsets {
3203 if let Some(schema_id) = AvroDeserializer::extract_confluent_id(
3204 &self.poll_payload_buf[start..start + len],
3205 ) {
3206 let is_new = avro_deser
3207 .ensure_schema_registered(schema_id)
3208 .await
3209 .map_err(|error| {
3210 terminalize_guaranteed_poll_error(
3211 self.delivery,
3212 &mut self.state,
3213 &self.metrics,
3214 self.reader_shutdown.as_ref(),
3215 error,
3216 )
3217 })?;
3218 if is_new {
3219 new_schema_ids.push(schema_id);
3220 }
3221 }
3222 }
3223
3224 if !new_schema_ids.is_empty()
3226 && self.config.schema_evolution_strategy != SchemaEvolutionStrategy::Ignore
3227 {
3228 if let Some(ref sr) = self.schema_registry {
3229 let compat = self
3230 .config
3231 .schema_compatibility
3232 .map_or(CompatibilityMode::Backward, CompatibilityMode::from);
3233 let evolver = SchemaEvolution::new(compat);
3234
3235 for id in new_schema_ids {
3236 let cached = sr.resolve_confluent_id(id).await.map_err(|error| {
3237 terminalize_guaranteed_poll_error(
3238 self.delivery,
3239 &mut self.state,
3240 &self.metrics,
3241 self.reader_shutdown.as_ref(),
3242 error,
3243 )
3244 })?;
3245
3246 let Some(ref prev) = self.last_avro_schema else {
3247 info!(schema_id = id, "initial Avro schema registered");
3249 self.last_avro_schema = Some(Arc::clone(&cached.arrow_schema));
3250 continue;
3251 };
3252
3253 let changes = evolver.diff_schemas(prev, &cached.arrow_schema);
3254 self.last_avro_schema = Some(Arc::clone(&cached.arrow_schema));
3255
3256 if changes.is_empty() {
3257 info!(
3258 schema_id = id,
3259 "new Avro schema ID registered, no field changes"
3260 );
3261 continue;
3262 }
3263 let verdict = evolver.evaluate_evolution(&changes);
3264 match &verdict {
3265 EvolutionVerdict::Compatible => {
3266 info!(schema_id = id, ?changes, "schema evolved (compatible)");
3267 }
3268 EvolutionVerdict::RequiresMigration => {
3269 warn!(
3270 schema_id = id,
3271 ?changes,
3272 "schema evolved (requires migration)"
3273 );
3274 }
3275 EvolutionVerdict::Incompatible(reason) => {
3276 if self.config.schema_evolution_strategy
3277 == SchemaEvolutionStrategy::Reject
3278 {
3279 let error = ConnectorError::SchemaMismatch(format!(
3280 "incompatible schema evolution for ID {id}: {reason}"
3281 ));
3282 return Err(terminalize_guaranteed_poll_error(
3283 self.delivery,
3284 &mut self.state,
3285 &self.metrics,
3286 self.reader_shutdown.as_ref(),
3287 error,
3288 ));
3289 }
3290 warn!(
3291 schema_id = id, %reason, ?changes,
3292 "incompatible schema evolution detected"
3293 );
3294 }
3295 }
3296 }
3297 }
3298 }
3299 }
3300
3301 let refs: Vec<&[u8]> = self
3302 .poll_payload_offsets
3303 .iter()
3304 .map(|&(start, len)| &self.poll_payload_buf[start..start + len])
3305 .collect();
3306
3307 let (batch, good_indices) = match self.deserializer.deserialize_batch(&refs, &self.schema) {
3310 Ok(batch) => (batch, None),
3311 Err(batch_err) if self.delivery != DeliveryGuarantee::BestEffort => {
3312 return Err(terminalize_guaranteed_poll_error(
3316 self.delivery,
3317 &mut self.state,
3318 &self.metrics,
3319 self.reader_shutdown.as_ref(),
3320 ConnectorError::Serde(batch_err),
3321 ));
3322 }
3323 Err(batch_err) => {
3324 let mut good_batches = Vec::with_capacity(refs.len());
3329 let mut good_idx = Vec::with_capacity(refs.len());
3330 let mut error_count = 0usize;
3331 for (i, r) in refs.iter().enumerate() {
3332 match self
3333 .deserializer
3334 .deserialize_batch(std::slice::from_ref(r), &self.schema)
3335 {
3336 Ok(batch) => {
3337 good_batches.push(batch);
3338 good_idx.push(i);
3339 }
3340 Err(e) => {
3341 error_count += 1;
3342 self.metrics.record_error();
3343 warn!(error = %e, "skipping poison pill record");
3344 }
3345 }
3346 }
3347 if good_batches.is_empty() {
3348 return Err(ConnectorError::Serde(batch_err));
3349 }
3350 if error_count > 0 {
3352 let error_count = u32::try_from(error_count).map_err(|_| {
3353 ConnectorError::Internal(
3354 "Kafka deserialization error count exceeds u32".into(),
3355 )
3356 })?;
3357 let record_count = u32::try_from(refs.len()).map_err(|_| {
3358 ConnectorError::Internal("Kafka batch record count exceeds u32".into())
3359 })?;
3360 let error_rate = f64::from(error_count) / f64::from(record_count);
3361 if error_rate > self.config.max_deser_error_rate {
3362 return Err(ConnectorError::Serde(batch_err));
3363 }
3364 warn!(
3365 skipped = error_count,
3366 total = refs.len(),
3367 error_rate = %format_args!("{error_rate:.1}"),
3368 "deserialized batch with poison pill isolation"
3369 );
3370 }
3371 let concat_schema = good_batches[0].schema();
3372 let batch = arrow_select::concat::concat_batches(&concat_schema, &good_batches)
3373 .map_err(|e| {
3374 ConnectorError::Internal(format!("failed to concat batches: {e}"))
3375 })?;
3376 (batch, Some(good_idx))
3377 }
3378 };
3379
3380 let expected_rows = good_indices.as_ref().map_or(refs.len(), Vec::len);
3383 if batch.num_rows() != expected_rows {
3384 let error = ConnectorError::Serde(SerdeError::RecordCountMismatch {
3385 expected: expected_rows,
3386 got: batch.num_rows(),
3387 });
3388 return Err(terminalize_guaranteed_poll_error(
3389 self.delivery,
3390 &mut self.state,
3391 &self.metrics,
3392 self.reader_shutdown.as_ref(),
3393 error,
3394 ));
3395 }
3396
3397 Ok((batch, good_indices))
3398 }
3399}
3400
3401impl KafkaSource {
3402 fn append_metadata_columns(
3403 &mut self,
3404 batch: arrow_array::RecordBatch,
3405 good_indices: Option<&[usize]>,
3406 include_metadata: bool,
3407 include_headers: bool,
3408 ) -> Result<arrow_array::RecordBatch, ConnectorError> {
3409 if let Some(idx) = good_indices {
3412 if include_metadata {
3413 self.poll_meta_partitions =
3414 idx.iter().map(|&i| self.poll_meta_partitions[i]).collect();
3415 self.poll_meta_offsets = idx.iter().map(|&i| self.poll_meta_offsets[i]).collect();
3416 self.poll_meta_timestamps =
3417 idx.iter().map(|&i| self.poll_meta_timestamps[i]).collect();
3418 }
3419 if include_headers {
3420 self.poll_meta_headers = idx
3421 .iter()
3422 .map(|&i| std::mem::take(&mut self.poll_meta_headers[i]))
3423 .collect();
3424 }
3425 }
3426
3427 let needs_meta = include_metadata && !self.poll_meta_partitions.is_empty();
3429 let needs_headers = include_headers && !self.poll_meta_headers.is_empty();
3430 let batch = if needs_meta || needs_headers {
3431 use arrow_schema::{DataType, Field};
3432
3433 let mut fields = batch.schema().fields().to_vec();
3434 let mut columns: Vec<Arc<dyn arrow_array::Array>> = batch.columns().to_vec();
3435
3436 if needs_meta {
3437 use arrow_array::{Int32Array, Int64Array, TimestampMillisecondArray};
3438 use arrow_schema::TimeUnit;
3439 fields.push(Arc::new(Field::new("_partition", DataType::Int32, false)));
3440 columns.push(Arc::new(Int32Array::from(std::mem::take(
3441 &mut self.poll_meta_partitions,
3442 ))));
3443 fields.push(Arc::new(Field::new("_offset", DataType::Int64, false)));
3444 columns.push(Arc::new(Int64Array::from(std::mem::take(
3445 &mut self.poll_meta_offsets,
3446 ))));
3447 fields.push(Arc::new(Field::new(
3448 "_timestamp",
3449 DataType::Timestamp(TimeUnit::Millisecond, None),
3450 true,
3451 )));
3452 columns.push(Arc::new(TimestampMillisecondArray::from(std::mem::take(
3453 &mut self.poll_meta_timestamps,
3454 ))));
3455 }
3456 if needs_headers {
3457 fields.push(Arc::new(Field::new("_headers", DataType::Utf8, true)));
3458 columns.push(Arc::new(arrow_array::StringArray::from(std::mem::take(
3459 &mut self.poll_meta_headers,
3460 ))));
3461 }
3462
3463 let meta_schema = Arc::new(arrow_schema::Schema::new(fields));
3464 arrow_array::RecordBatch::try_new(meta_schema, columns).map_err(|e| {
3465 terminalize_guaranteed_poll_error(
3466 self.delivery,
3467 &mut self.state,
3468 &self.metrics,
3469 self.reader_shutdown.as_ref(),
3470 ConnectorError::Internal(format!("failed to append metadata columns: {e}")),
3471 )
3472 })?
3473 } else {
3474 batch
3475 };
3476
3477 Ok(batch)
3478 }
3479}
3480impl KafkaSource {
3481 fn prepare_start(&mut self, request: SourceStart) -> Result<KafkaStartPlan, ConnectorError> {
3482 let (config, position, delivery) = request.into_parts();
3483
3484 let kafka_config = if config.properties().is_empty() {
3488 self.config.clone()
3489 } else {
3490 KafkaSourceConfig::from_config(&config)?
3491 };
3492 let (installed_offsets, resume_attempt, is_resume, resume_inventory, resume_baselines) =
3493 match position {
3494 SourcePosition::Initial => (
3495 OffsetTracker::new(),
3496 None,
3497 false,
3498 None,
3499 KafkaPartitionBaselines::new(),
3500 ),
3501 SourcePosition::Resume {
3502 attempt,
3503 checkpoint,
3504 } => {
3505 let inventory = decode_partition_inventory(&checkpoint)?;
3506 let baselines = decode_partition_baselines(&checkpoint)?;
3507 (
3508 OffsetTracker::try_from_checkpoint(&checkpoint)?,
3509 Some(attempt),
3510 true,
3511 inventory,
3512 baselines,
3513 )
3514 }
3515 };
3516 if (self.vnode_assignment.is_some() || delivery != DeliveryGuarantee::BestEffort)
3517 && matches!(&kafka_config.subscription, TopicSubscription::Pattern(_))
3518 {
3519 return Err(ConnectorError::ConfigurationError(
3520 "Kafka topic patterns are unsupported with engine-owned assignment; declare the \
3521 exact topic inventory so ownership and checkpoint cuts stay stable"
3522 .into(),
3523 ));
3524 }
3525 if matches!(
3526 &kafka_config.startup_mode,
3527 StartupMode::SpecificOffsets(_) | StartupMode::Timestamp(_)
3528 ) {
3529 if matches!(&kafka_config.subscription, TopicSubscription::Pattern(_)) {
3530 return Err(ConnectorError::ConfigurationError(
3531 "Kafka specific-offset/timestamp startup requires an explicit topic list"
3532 .into(),
3533 ));
3534 }
3535 if self.vnode_assignment.is_some() {
3536 return Err(ConnectorError::ConfigurationError(
3537 "Kafka specific-offset/timestamp startup is unsupported with vnode assignment"
3538 .into(),
3539 ));
3540 }
3541 }
3542 if let StartupMode::SpecificOffsets(offsets) = &kafka_config.startup_mode {
3543 if let Some((&partition, &offset)) = offsets
3544 .iter()
3545 .find(|(partition, offset)| **partition < 0 || **offset < 0)
3546 {
3547 return Err(ConnectorError::ConfigurationError(format!(
3548 "invalid Kafka specific position {partition}:{offset}: partition and offset must be non-negative"
3549 )));
3550 }
3551 }
3552 if delivery != DeliveryGuarantee::BestEffort
3553 && matches!(&kafka_config.startup_mode, StartupMode::GroupOffsets)
3554 {
3555 return Err(ConnectorError::ConfigurationError(
3556 "Kafka guaranteed delivery cannot use broker group offsets as its initial recovery \
3557 authority; another group member can advance that cursor before LaminarDB seals \
3558 it. Use earliest or explicit specific offsets"
3559 .into(),
3560 ));
3561 }
3562 if delivery != DeliveryGuarantee::BestEffort
3563 && matches!(
3564 &kafka_config.startup_mode,
3565 StartupMode::Latest | StartupMode::Timestamp(_)
3566 )
3567 {
3568 return Err(ConnectorError::ConfigurationError(
3569 "Kafka guaranteed delivery requires a stable unrecorded-partition start; latest \
3570 and timestamp-with-no-match can move forward across recovery. Use earliest or \
3571 explicit specific offsets until checkpointed partition baselines are available"
3572 .into(),
3573 ));
3574 }
3575
3576 let deterministic_unrecorded = is_resume || delivery != DeliveryGuarantee::BestEffort;
3581 let configured_source_name = config.get("laminar.source.name");
3582 if self.vnode_assignment.is_some() {
3583 if configured_source_name
3584 .is_some_and(|configured| configured != self.source_name.as_ref())
3585 {
3586 return Err(ConnectorError::ConfigurationError(format!(
3587 "Kafka vnode assignment identity '{}' does not match canonical source name \
3588 '{}'",
3589 self.source_name,
3590 configured_source_name.unwrap_or_default()
3591 )));
3592 }
3593 } else {
3594 self.source_name = Arc::from(configured_source_name.unwrap_or_default());
3595 }
3596 self.state = ConnectorState::Initializing;
3597 self.config = kafka_config.clone();
3598 self.delivery = delivery;
3599 self.offsets = installed_offsets;
3600 self.manual_topic_partitions.clear();
3601 self.manual_partition_baselines.clear();
3602 *lock_or_recover(&self.assignment_publication) =
3603 Arc::new(KafkaAssignmentPublication::default());
3604 self.rotation_partition_baseline_count
3605 .store(0, Ordering::Release);
3606 self.applied_rotation_baseline_version = None;
3607 self.reconciled_assignment_version
3608 .store(0, Ordering::Release);
3609 lock_or_recover(&self.offset_snapshot).clone_from(&self.offsets);
3610 self.deterministic_unrecorded_position
3611 .store(deterministic_unrecorded, Ordering::Release);
3612 if let Some(attempt) = resume_attempt {
3613 self.assign_generation.fetch_add(1, Ordering::Release);
3616 info!(
3617 epoch = attempt.epoch,
3618 checkpoint_id = attempt.checkpoint_id,
3619 partition_count = self.offsets.partition_count(),
3620 "installed exact Kafka resume position before consumer activation"
3621 );
3622 }
3623
3624 if let Some(sr_client) = Self::build_sr_client(&kafka_config)? {
3626 let sr = Arc::new(sr_client);
3627 self.schema_registry = Some(Arc::clone(&sr));
3628 self.deserializer = if kafka_config.format == Format::Avro {
3629 Box::new(AvroDeserializer::with_schema_registry(sr))
3630 } else {
3631 select_deserializer(kafka_config.format)
3632 };
3633 } else if let Some(ref sr) = self.schema_registry {
3634 self.deserializer = if kafka_config.format == Format::Avro {
3636 Box::new(AvroDeserializer::with_schema_registry(Arc::clone(sr)))
3637 } else {
3638 select_deserializer(kafka_config.format)
3639 };
3640 } else {
3641 self.deserializer = select_deserializer(kafka_config.format);
3642 }
3643
3644 self.last_avro_schema = None;
3646
3647 if let Some(schema) = config.arrow_schema() {
3649 info!(
3650 fields = schema.fields().len(),
3651 "using SQL-defined schema for deserialization"
3652 );
3653 self.schema = schema;
3654 }
3655
3656 info!(
3657 brokers = %kafka_config.bootstrap_servers,
3658 subscription = ?kafka_config.subscription,
3659 group_id = %kafka_config.group_id,
3660 format = %kafka_config.format,
3661 schema_fields = self.schema.fields().len(),
3662 "starting Kafka source connector"
3663 );
3664
3665 Ok(KafkaStartPlan {
3666 config: kafka_config,
3667 delivery,
3668 is_resume,
3669 resume_inventory,
3670 resume_baselines,
3671 })
3672 }
3673}
3674impl KafkaSource {
3675 async fn assign_vnode_partitions(
3676 &mut self,
3677 consumer: &Arc<StreamConsumer<LaminarConsumerContext>>,
3678 config: &KafkaSourceConfig,
3679 delivery: DeliveryGuarantee,
3680 is_resume: bool,
3681 resume_baselines: &KafkaPartitionBaselines,
3682 ) -> Result<bool, ConnectorError> {
3683 self.vnode_partition_routes.clear();
3692 let vnode = self
3693 .vnode_assignment
3694 .as_ref()
3695 .map(|(r, s)| (Arc::clone(r), *s));
3696 let vnode_assigned = if let Some((registry, self_id)) = vnode {
3697 if let TopicSubscription::Topics(topics) = &config.subscription {
3698 let topic_meta = fetch_explicit_topic_metadata(
3699 self.blocking_tasks.clone(),
3700 Arc::clone(consumer),
3701 topics.clone(),
3702 )
3703 .await?;
3704 let partition_routes = kafka_partition_routes(
3705 self.source_name.as_ref(),
3706 registry.vnode_count(),
3707 &topic_meta,
3708 )?;
3709 let all_partitions: KafkaPartitionSet = topic_meta
3710 .iter()
3711 .flat_map(|(topic, count)| {
3712 (0..*count).map(move |partition| (topic.to_string(), partition))
3713 })
3714 .collect();
3715 if let Some(unexpected) = self
3716 .offsets
3717 .to_topic_partition_list()
3718 .elements()
3719 .iter()
3720 .find(|entry| {
3721 !all_partitions.contains(&(entry.topic().to_string(), entry.partition()))
3722 })
3723 {
3724 return Err(ConnectorError::ConfigurationError(format!(
3725 "Kafka resume checkpoint references partition '{}-{}' absent from the explicit topic inventory",
3726 unexpected.topic(),
3727 unexpected.partition()
3728 )));
3729 }
3730 let requires_numeric_cut = delivery != DeliveryGuarantee::BestEffort;
3731 if requires_numeric_cut {
3732 let low_watermarks = fetch_partition_low_watermarks(
3733 self.blocking_tasks.clone(),
3734 Arc::clone(consumer),
3735 &all_partitions,
3736 )
3737 .await?;
3738 let baselines = if is_resume {
3739 validate_partition_baselines(resume_baselines, &all_partitions)?;
3740 resume_baselines.clone()
3741 } else {
3742 low_watermarks.clone()
3743 };
3744 validate_positions_not_expired(
3745 &self.offsets,
3746 &baselines,
3747 &low_watermarks,
3748 &all_partitions,
3749 )?;
3750 self.manual_partition_baselines = baselines;
3751 }
3752 self.manual_topic_partitions = all_partitions;
3753 let default_offset = if self
3754 .deterministic_unrecorded_position
3755 .load(Ordering::Acquire)
3756 {
3757 deterministic_initial_offset(&config.startup_mode, config.auto_offset_reset)
3758 .ok_or_else(|| {
3759 ConnectorError::ConfigurationError(
3760 "Kafka startup mode has no deterministic vnode fallback".into(),
3761 )
3762 })?
3763 } else {
3764 startup_default_offset(&config.startup_mode)
3765 };
3766 let no_resume = OffsetTracker::new();
3769 let no_resume_baselines = KafkaPartitionBaselines::new();
3770 let published = registry.read_assignment();
3773 let assignment_version = published.version();
3774 let boot_unassigned = kafka_bootstrap_is_unassigned(&published, self_id)?;
3775 let tpl = if boot_unassigned {
3776 TopicPartitionList::new()
3777 } else {
3778 build_vnode_assignment_tpl(
3779 self.source_name.as_ref(),
3780 published.owners(),
3781 self_id,
3782 &topic_meta,
3783 VnodeResumeCursors {
3784 local_offsets: &self.offsets,
3785 handoff_offsets: &no_resume,
3786 local_baselines: &self.manual_partition_baselines,
3787 handoff_baselines: &no_resume_baselines,
3788 },
3789 default_offset,
3790 )?
3791 };
3792 let owned_partitions = Arc::new(
3793 kafka_partition_set(&tpl).map_err(ConnectorError::ConfigurationError)?,
3794 );
3795 if tpl.count() > 0 {
3798 consumer.incremental_assign(&tpl).map_err(|e| {
3799 ConnectorError::ConnectionFailed(format!(
3800 "vnode partition assign failed: {e}"
3801 ))
3802 })?;
3803 }
3804 validate_kafka_partition_results("initial incremental assign", &tpl)
3805 .map_err(ConnectorError::ConnectionFailed)?;
3806 let active = consumer.assignment().map_err(|error| {
3807 ConnectorError::ConnectionFailed(format!(
3808 "failed to inspect initial vnode assignment: {error}"
3809 ))
3810 })?;
3811 let active =
3812 kafka_partition_set(&active).map_err(ConnectorError::ConnectionFailed)?;
3813 validate_kafka_assignment(&owned_partitions, &active)
3814 .map_err(ConnectorError::ConnectionFailed)?;
3815 self.vnode_partition_routes = partition_routes;
3816 *lock_or_recover(&self.assignment_publication) =
3817 Arc::new(KafkaAssignmentPublication::new(
3818 assignment_version,
3819 Arc::clone(&owned_partitions),
3820 KafkaRotationBaselines::new(),
3821 ));
3822 self.reconciled_assignment_version
3823 .store(assignment_version, Ordering::Release);
3824 drop(published);
3825 if boot_unassigned {
3826 info!(
3827 "Kafka source started fenced with no partitions until durable vnode adoption"
3828 );
3829 } else {
3830 info!(
3831 owned_partitions = owned_partitions.len(),
3832 "Kafka source assigned vnode-owned partitions (engine-controlled)"
3833 );
3834 }
3835 true
3836 } else {
3837 return Err(ConnectorError::ConfigurationError(
3838 "Kafka vnode assignment requires an explicit topic inventory".into(),
3839 ));
3840 }
3841 } else {
3842 false
3843 };
3844
3845 Ok(vnode_assigned)
3846 }
3847}
3848impl KafkaSource {
3849 async fn assign_local_guaranteed_partitions(
3850 &mut self,
3851 consumer: &Arc<StreamConsumer<LaminarConsumerContext>>,
3852 config: &KafkaSourceConfig,
3853 delivery: DeliveryGuarantee,
3854 vnode_assigned: bool,
3855 is_resume: bool,
3856 resume_inventory: Option<&KafkaPartitionSet>,
3857 resume_baselines: &KafkaPartitionBaselines,
3858 ) -> Result<bool, ConnectorError> {
3859 let local_guaranteed_assignment = delivery != DeliveryGuarantee::BestEffort
3860 && !vnode_assigned
3861 && matches!(&config.startup_mode, StartupMode::Earliest);
3862 if local_guaranteed_assignment {
3863 let TopicSubscription::Topics(topics) = &config.subscription else {
3864 return Err(ConnectorError::ConfigurationError(
3865 "Kafka guaranteed delivery requires an explicit topic inventory".into(),
3866 ));
3867 };
3868 let topic_meta = fetch_explicit_topic_metadata(
3869 self.blocking_tasks.clone(),
3870 Arc::clone(consumer),
3871 topics.clone(),
3872 )
3873 .await?;
3874 let assigned: Vec<(String, i32)> = topic_meta
3875 .iter()
3876 .flat_map(|(topic, count)| {
3877 (0..*count).map(move |partition| (topic.to_string(), partition))
3878 })
3879 .collect();
3880 let assigned_set: KafkaPartitionSet = assigned.iter().cloned().collect();
3881 if let Some(unexpected) = self
3882 .offsets
3883 .to_topic_partition_list()
3884 .elements()
3885 .iter()
3886 .find(|entry| {
3887 !assigned_set.contains(&(entry.topic().to_string(), entry.partition()))
3888 })
3889 {
3890 return Err(ConnectorError::ConfigurationError(format!(
3891 "Kafka resume checkpoint references partition '{}-{}' absent from the explicit topic inventory",
3892 unexpected.topic(),
3893 unexpected.partition()
3894 )));
3895 }
3896 let low_watermarks = fetch_partition_low_watermarks(
3897 self.blocking_tasks.clone(),
3898 Arc::clone(consumer),
3899 &assigned_set,
3900 )
3901 .await?;
3902 let baselines = if is_resume {
3903 validate_resume_inventory(resume_inventory, &assigned_set)?;
3904 validate_partition_baselines(resume_baselines, &assigned_set)?;
3905 resume_baselines.clone()
3906 } else {
3907 low_watermarks.clone()
3908 };
3909 validate_positions_not_expired(
3910 &self.offsets,
3911 &baselines,
3912 &low_watermarks,
3913 &assigned_set,
3914 )?;
3915 let assignment =
3916 assignment_seek_tpl(&self.offsets, &assigned, Some(&baselines), None, true)?;
3917 consumer.assign(&assignment).map_err(|error| {
3918 ConnectorError::ConnectionFailed(format!(
3919 "failed to install local guaranteed Kafka assignment: {error}"
3920 ))
3921 })?;
3922 self.manual_topic_partitions = assigned_set;
3923 self.manual_partition_baselines = baselines;
3924 info!(
3925 partition_count = assignment.count(),
3926 "Kafka source assigned full explicit inventory (local guaranteed delivery)"
3927 );
3928 }
3929
3930 Ok(local_guaranteed_assignment)
3931 }
3932}
3933impl KafkaSource {
3934 async fn prefetch_schema_registry(
3935 &mut self,
3936 config: &KafkaSourceConfig,
3937 ) -> Result<(), ConnectorError> {
3938 if let Some(ref sr) = self.schema_registry {
3941 if let TopicSubscription::Topics(topics) = &config.subscription {
3942 if topics.len() > 1 {
3943 warn!("multiple topics with schema registry — using first topic's schema");
3944 }
3945 if let Some(topic) = topics.first() {
3946 let subject = resolve_value_subject(
3947 config.schema_registry_subject_strategy,
3948 config.schema_registry_record_name.as_deref(),
3949 topic,
3950 );
3951 match tokio::time::timeout(
3952 config.schema_registry_discovery_timeout,
3953 sr.get_latest_schema(&subject),
3954 )
3955 .await
3956 {
3957 Ok(Ok(cached)) => {
3958 if let Some(avro_deser) = self
3959 .deserializer
3960 .as_any_mut()
3961 .and_then(|any| any.downcast_mut::<AvroDeserializer>())
3962 {
3963 if let Err(error) =
3964 avro_deser.register_schema(cached.id, &cached.schema_str)
3965 {
3966 let error = ConnectorError::Serde(error);
3967 self.fail_startup();
3968 return Err(error);
3969 }
3970 log_schema_drift(&self.schema, &cached.arrow_schema, &subject);
3973 info!(%subject, schema_id = cached.id,
3974 "SR schema fetched at start()");
3975 self.last_avro_schema = Some(cached.arrow_schema);
3976 }
3977 }
3978 Ok(Err(e)) if e.is_transient() => {
3979 warn!(%subject, error = %e, "SR unavailable at start(), will resolve lazily");
3980 }
3981 Ok(Err(e)) => {
3982 self.fail_startup();
3983 return Err(e);
3984 }
3985 Err(_elapsed) => {
3986 warn!(%subject, "SR prefetch timed out at start(), will resolve lazily");
3987 }
3988 }
3989 }
3990 }
3991 }
3992
3993 Ok(())
3994 }
3995}
3996#[async_trait]
3997impl SourceConnector for KafkaSource {
3998 fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
3999 Some(self.task_tracker.clone())
4000 }
4001
4002 fn contract(&self, _config: &ConnectorConfig) -> Result<SourceContract, ConnectorError> {
4003 Ok(SourceContract::new(
4004 SourceConsistency::Replayable,
4005 SourceTopology::Splittable,
4006 ))
4007 }
4008
4009 fn set_vnode_assignment(
4010 &mut self,
4011 source_identity: &str,
4012 registry: Arc<laminar_core::state::VnodeRegistry>,
4013 self_id: laminar_core::state::NodeId,
4014 ) -> Result<(), ConnectorError> {
4015 if source_identity.is_empty() {
4016 return Err(ConnectorError::ConfigurationError(
4017 "Kafka vnode assignment requires a non-empty canonical source identity".into(),
4018 ));
4019 }
4020 if self_id.is_unassigned() {
4021 return Err(ConnectorError::ConfigurationError(
4022 "Kafka vnode assignment requires a nonzero node identity".into(),
4023 ));
4024 }
4025 info!(
4026 source = source_identity,
4027 self_id = self_id.0,
4028 vnode_count = registry.vnode_count(),
4029 "Kafka source: engine-controlled partition→vnode assignment enabled"
4030 );
4031 self.source_name = Arc::from(source_identity);
4032 self.vnode_assignment = Some((registry, self_id));
4033 Ok(())
4034 }
4035
4036 fn begin_drain(
4037 &mut self,
4038 request: &SourceDrainRequest,
4039 deadline: tokio::time::Instant,
4040 ) -> Result<(), ConnectorError> {
4041 self.check_reader_health("starting a global source drain")?;
4042 if let Some(active) = self.source_drain.as_ref() {
4043 if active.request != *request {
4044 return Err(ConnectorError::InvalidState {
4045 expected: format!("active Kafka drain {:?}", active.request.round),
4046 actual: format!("conflicting Kafka drain {:?}", request.round),
4047 });
4048 }
4049 return Ok(());
4052 }
4053 if tokio::time::Instant::now() >= deadline {
4054 return Err(ConnectorError::Internal(
4055 "Kafka source drain began after its engine deadline".into(),
4056 ));
4057 }
4058 let Some((registry, _)) = self.vnode_assignment.as_ref() else {
4059 return Err(ConnectorError::InvalidState {
4060 expected: "Kafka cluster assignment installed before source drain".into(),
4061 actual: "embedded/single Kafka source".into(),
4062 });
4063 };
4064 if registry.assignment_version() != request.round.predecessor_version {
4065 return Err(ConnectorError::InvalidState {
4066 expected: format!(
4067 "Kafka predecessor assignment {}",
4068 request.round.predecessor_version
4069 ),
4070 actual: registry.assignment_version().to_string(),
4071 });
4072 }
4073 let reconciled = self.reconciled_assignment_version.load(Ordering::Acquire);
4074 if reconciled != request.round.predecessor_version {
4075 return Err(ConnectorError::InvalidState {
4076 expected: format!(
4077 "reconciled Kafka predecessor assignment {}",
4078 request.round.predecessor_version
4079 ),
4080 actual: reconciled.to_string(),
4081 });
4082 }
4083 self.ensure_reader_started();
4084 let tx = self.reader_drain_tx.as_ref().ok_or_else(|| {
4085 ConnectorError::Internal("Kafka cluster reader has no drain control channel".into())
4086 })?;
4087 tx.send(KafkaReaderDrainCommand::Begin {
4088 request: request.clone(),
4089 deadline,
4090 })
4091 .map_err(|_| ConnectorError::Internal("Kafka reader drain channel closed".into()))?;
4092 self.source_drain = Some(KafkaSourceDrain {
4093 request: request.clone(),
4094 prepare_deadline: deadline,
4095 boundary: None,
4096 cut: None,
4097 pending_resolution: None,
4098 });
4099 self.data_ready.notify_one();
4100 Ok(())
4101 }
4102
4103 fn poll_drain_ready(
4104 &mut self,
4105 round: laminar_core::checkpoint::AssignmentDrainId,
4106 ) -> Result<bool, ConnectorError> {
4107 self.check_reader_health("capturing a global source drain cut")?;
4108 let Some(active) = self.source_drain.as_ref() else {
4109 return Err(ConnectorError::InvalidState {
4110 expected: format!("active Kafka drain {round:?}"),
4111 actual: "no Kafka drain".into(),
4112 });
4113 };
4114 if active.request.round != round {
4115 return Err(ConnectorError::InvalidState {
4116 expected: format!("active Kafka drain {:?}", active.request.round),
4117 actual: format!("cut requested for {round:?}"),
4118 });
4119 }
4120 if active.cut.is_some() {
4121 return Ok(true);
4122 }
4123 if tokio::time::Instant::now() >= active.prepare_deadline {
4124 return Err(ConnectorError::Internal(
4125 "Kafka drain deadline expired before cursor capture".into(),
4126 ));
4127 }
4128 let Some(boundary) = active.boundary.clone() else {
4129 return Ok(false);
4130 };
4131 let cut = self.capture_drain_positions(&boundary.inputs, Some(active.prepare_deadline))?;
4132 let active = self.source_drain.as_mut().expect("checked above");
4133 active.cut = Some(cut);
4134 Ok(true)
4135 }
4136
4137 async fn finish_drain(
4138 &mut self,
4139 resolution: SourceDrainResolution,
4140 deadline: tokio::time::Instant,
4141 ) -> Result<(), ConnectorError> {
4142 self.check_reader_health("resolving a global source drain")?;
4143 let active = self
4144 .source_drain
4145 .as_ref()
4146 .ok_or_else(|| ConnectorError::InvalidState {
4147 expected: format!("active Kafka drain {:?}", resolution.round),
4148 actual: "no Kafka drain".into(),
4149 })?;
4150 if active.request.round != resolution.round {
4151 return Err(ConnectorError::InvalidState {
4152 expected: format!("active Kafka drain {:?}", active.request.round),
4153 actual: format!("resolution for {:?}", resolution.round),
4154 });
4155 }
4156 let cut =
4157 active
4158 .cut
4159 .as_ref()
4160 .map(Arc::clone)
4161 .ok_or_else(|| ConnectorError::InvalidState {
4162 expected: "Kafka FIFO drain cut before resolution".into(),
4163 actual: "drain boundary not consumed".into(),
4164 })?;
4165 if let Some(pending) = active.pending_resolution.as_ref() {
4166 if pending.resolution != resolution {
4167 return Err(ConnectorError::InvalidState {
4168 expected: format!("pending Kafka resolution {:?}", pending.resolution),
4169 actual: format!("conflicting Kafka resolution {resolution:?}"),
4170 });
4171 }
4172 if let Some(error) = pending.terminal_error.as_ref() {
4175 return Err(ConnectorError::Internal(error.to_string()));
4176 }
4177 if pending.execution.load(Ordering::Acquire) == KAFKA_DRAIN_EXECUTION_CANCELLED {
4178 return Err(ConnectorError::Internal(
4179 "Kafka drain resolution was cancelled before execution".into(),
4180 ));
4181 }
4182 } else {
4183 if tokio::time::Instant::now() >= deadline {
4184 return Err(ConnectorError::Internal(
4185 "Kafka drain engine deadline expired before resolution".into(),
4186 ));
4187 }
4188 let tx = self.reader_drain_tx.as_ref().ok_or_else(|| {
4189 ConnectorError::Internal("Kafka reader drain channel is absent".into())
4190 })?;
4191 let execution = Arc::new(AtomicU8::new(KAFKA_DRAIN_EXECUTION_PENDING));
4192 let (reply, result) = tokio::sync::oneshot::channel();
4193 tx.send(KafkaReaderDrainCommand::Resolve {
4194 resolution,
4195 cut,
4196 deadline,
4197 execution: Arc::clone(&execution),
4198 reply,
4199 })
4200 .map_err(|_| ConnectorError::Internal("Kafka reader drain channel closed".into()))?;
4201 self.source_drain
4202 .as_mut()
4203 .expect("active drain was validated above")
4204 .pending_resolution = Some(KafkaPendingDrainResolution {
4205 resolution,
4206 deadline,
4207 execution,
4208 reply: result,
4209 terminal_error: None,
4210 });
4211 self.data_ready.notify_one();
4212 }
4213 let pending = self
4214 .source_drain
4215 .as_mut()
4216 .and_then(|active| active.pending_resolution.as_mut())
4217 .expect("pending Kafka resolution was installed above");
4218 let execution = Arc::clone(&pending.execution);
4219 let mut wait_guard = KafkaDrainWaitGuard::new(Arc::clone(&execution));
4220 let reply = match tokio::time::timeout_at(pending.deadline, &mut pending.reply).await {
4221 Ok(reply) => reply,
4222 Err(_) => match execution.compare_exchange(
4223 KAFKA_DRAIN_EXECUTION_PENDING,
4224 KAFKA_DRAIN_EXECUTION_CANCELLED,
4225 Ordering::AcqRel,
4226 Ordering::Acquire,
4227 ) {
4228 Ok(_) | Err(KAFKA_DRAIN_EXECUTION_CANCELLED) => {
4229 let error: Arc<str> = Arc::from(
4230 "Kafka drain engine deadline expired before resolution completed",
4231 );
4232 pending.terminal_error = Some(Arc::clone(&error));
4233 wait_guard.disarm();
4234 return Err(ConnectorError::Internal(error.to_string()));
4235 }
4236 Err(KAFKA_DRAIN_EXECUTION_STARTED) => (&mut pending.reply).await,
4237 Err(state) => {
4238 wait_guard.disarm();
4239 return Err(ConnectorError::Internal(format!(
4240 "Kafka drain resolution has invalid execution state {state}"
4241 )));
4242 }
4243 },
4244 };
4245 wait_guard.disarm();
4246 match reply {
4247 Ok(Ok(())) => {
4248 self.source_drain = None;
4249 Ok(())
4250 }
4251 Ok(Err(error)) => {
4252 pending.terminal_error = Some(Arc::from(error.as_str()));
4253 Err(ConnectorError::Internal(error))
4254 }
4255 Err(_) => {
4256 let error: Arc<str> = Arc::from("Kafka reader dropped drain resolution");
4257 pending.terminal_error = Some(Arc::clone(&error));
4258 Err(ConnectorError::Internal(error.to_string()))
4259 }
4260 }
4261 }
4262
4263 async fn start(&mut self, request: SourceStart) -> Result<(), ConnectorError> {
4264 if self.state != ConnectorState::Created {
4265 return Err(ConnectorError::InvalidState {
4266 expected: ConnectorState::Created.to_string(),
4267 actual: self.state.to_string(),
4268 });
4269 }
4270 let KafkaStartPlan {
4271 config: kafka_config,
4272 delivery,
4273 is_resume,
4274 resume_inventory,
4275 resume_baselines,
4276 } = self.prepare_start(request)?;
4277 let mut rdkafka_config: ClientConfig = kafka_config.to_rdkafka_config();
4278 if delivery != DeliveryGuarantee::BestEffort
4279 || matches!(
4280 &kafka_config.startup_mode,
4281 StartupMode::SpecificOffsets(_) | StartupMode::Timestamp(_)
4282 )
4283 {
4284 rdkafka_config.set("auto.offset.reset", "error");
4288 }
4289 let context = LaminarConsumerContext::new(
4290 Arc::clone(&self.rebalance_state),
4291 Arc::clone(&self.rebalance_counter),
4292 Arc::clone(&self.revoke_generation),
4293 Arc::clone(&self.assign_generation),
4294 self.metrics.commits.clone(),
4298 self.metrics.commit_failures.clone(),
4299 );
4300 let consumer: StreamConsumer<LaminarConsumerContext> = rdkafka_config
4301 .create_with_context(context)
4302 .map_err(|error| consumer_creation_error(&error))?;
4303 let consumer = Arc::new(consumer);
4307 self.consumer = Some(Arc::clone(&consumer));
4308
4309 let vnode_assigned = self
4310 .assign_vnode_partitions(
4311 &consumer,
4312 &kafka_config,
4313 delivery,
4314 is_resume,
4315 &resume_baselines,
4316 )
4317 .await?;
4318 let local_guaranteed_assignment = self
4319 .assign_local_guaranteed_partitions(
4320 &consumer,
4321 &kafka_config,
4322 delivery,
4323 vnode_assigned,
4324 is_resume,
4325 resume_inventory.as_ref(),
4326 &resume_baselines,
4327 )
4328 .await?;
4329 if !vnode_assigned && !local_guaranteed_assignment {
4333 match &kafka_config.startup_mode {
4334 StartupMode::GroupOffsets | StartupMode::Earliest | StartupMode::Latest => {
4335 match &kafka_config.subscription {
4336 TopicSubscription::Topics(topics) => {
4337 let topic_refs: Vec<&str> = topics.iter().map(String::as_str).collect();
4338 consumer.subscribe(&topic_refs).map_err(|e| {
4339 ConnectorError::ConnectionFailed(format!(
4340 "failed to subscribe: {e}"
4341 ))
4342 })?;
4343 }
4344 TopicSubscription::Pattern(pattern) => {
4345 let regex_pattern = if pattern.starts_with('^') {
4346 pattern.clone()
4347 } else {
4348 format!("^{pattern}")
4349 };
4350 consumer.subscribe(&[®ex_pattern]).map_err(|e| {
4351 ConnectorError::ConnectionFailed(format!(
4352 "failed to subscribe to pattern: {e}"
4353 ))
4354 })?;
4355 }
4356 }
4357 }
4358 StartupMode::SpecificOffsets(offsets) => {
4359 let TopicSubscription::Topics(topics) = &kafka_config.subscription else {
4360 return Err(ConnectorError::ConfigurationError(
4361 "Kafka specific-offset startup requires an explicit topic inventory"
4362 .into(),
4363 ));
4364 };
4365 let mut assigned: Vec<(String, i32)> = topics
4366 .iter()
4367 .flat_map(|topic| {
4368 offsets
4369 .keys()
4370 .copied()
4371 .map(move |partition| (topic.clone(), partition))
4372 })
4373 .collect();
4374 assigned.sort_unstable();
4375 if assigned.is_empty() {
4376 return Err(ConnectorError::ConfigurationError(
4377 "Kafka specific-offset startup resolved no partitions".into(),
4378 ));
4379 }
4380 let assigned_set: KafkaPartitionSet = assigned.iter().cloned().collect();
4381 let configured_baselines: KafkaPartitionBaselines = topics
4382 .iter()
4383 .flat_map(|topic| {
4384 offsets
4385 .iter()
4386 .map(move |(&partition, &next)| ((topic.clone(), partition), next))
4387 })
4388 .collect();
4389 let baselines = if is_resume {
4390 validate_resume_inventory(resume_inventory.as_ref(), &assigned_set)?;
4391 validate_partition_baselines(&resume_baselines, &assigned_set)?;
4392 if resume_baselines != configured_baselines {
4393 return Err(ConnectorError::ConfigurationError(
4394 "Kafka specific-offset configuration changed across recovery"
4395 .into(),
4396 ));
4397 }
4398 resume_baselines.clone()
4399 } else {
4400 configured_baselines
4401 };
4402 let low_watermarks = fetch_partition_low_watermarks(
4403 self.blocking_tasks.clone(),
4404 Arc::clone(&consumer),
4405 &assigned_set,
4406 )
4407 .await?;
4408 validate_positions_not_expired(
4409 &self.offsets,
4410 &baselines,
4411 &low_watermarks,
4412 &assigned_set,
4413 )?;
4414 let assignment = assignment_seek_tpl(
4415 &self.offsets,
4416 &assigned,
4417 Some(&baselines),
4418 None,
4419 true,
4420 )?;
4421 consumer.assign(&assignment).map_err(|e| {
4422 ConnectorError::ConnectionFailed(format!(
4423 "failed to assign specific offsets: {e}"
4424 ))
4425 })?;
4426 self.manual_topic_partitions = assigned_set;
4427 self.manual_partition_baselines = baselines;
4428 info!(
4429 partition_count = assignment.count(),
4430 "assigned consumer to exact checkpoint/specific offsets"
4431 );
4432 }
4433 StartupMode::Timestamp(ts_ms) => {
4434 let TopicSubscription::Topics(topics) = &kafka_config.subscription else {
4435 return Err(ConnectorError::ConfigurationError(
4436 "Kafka timestamp startup requires an explicit topic inventory".into(),
4437 ));
4438 };
4439 let topic_meta = fetch_explicit_topic_metadata(
4440 self.blocking_tasks.clone(),
4441 Arc::clone(&consumer),
4442 topics.clone(),
4443 )
4444 .await?;
4445 let mut tpl = rdkafka::TopicPartitionList::new();
4446 for (topic, partition_count) in &topic_meta {
4447 for partition in 0..*partition_count {
4448 tpl.add_partition_offset(
4449 topic,
4450 partition,
4451 rdkafka::Offset::Offset(*ts_ms),
4452 )
4453 .map_err(|error| {
4454 ConnectorError::Internal(format!(
4455 "failed to build timestamp lookup for '{topic}-{partition}': {error}"
4456 ))
4457 })?;
4458 }
4459 }
4460 if tpl.count() == 0 {
4461 return Err(ConnectorError::ConfigurationError(
4462 "Kafka timestamp startup discovered no partitions".into(),
4463 ));
4464 }
4465 let resolved = resolve_timestamp_offsets(
4466 self.blocking_tasks.clone(),
4467 Arc::clone(&consumer),
4468 tpl,
4469 )
4470 .await?;
4471 let mut positioned = TopicPartitionList::new();
4472 let mut restored = 0usize;
4473 for elem in resolved.elements() {
4474 if let Err(e) = elem.error() {
4475 return Err(ConnectorError::ConnectionFailed(format!(
4476 "timestamp lookup failed for '{}-{}': {e}",
4477 elem.topic(),
4478 elem.partition()
4479 )));
4480 }
4481 let offset = if let Some(checkpointed) =
4482 self.offsets.get(elem.topic(), elem.partition())
4483 {
4484 restored += 1;
4485 rdkafka::Offset::Offset(checkpointed.checked_add(1).ok_or_else(
4486 || {
4487 ConnectorError::ConfigurationError(format!(
4488 "Kafka checkpoint offset overflow for '{}-{}'",
4489 elem.topic(),
4490 elem.partition()
4491 ))
4492 },
4493 )?)
4494 } else if elem.offset() == rdkafka::Offset::Invalid {
4495 rdkafka::Offset::End
4498 } else {
4499 elem.offset()
4500 };
4501 positioned
4502 .add_partition_offset(elem.topic(), elem.partition(), offset)
4503 .map_err(|e| {
4504 ConnectorError::Internal(format!(
4505 "failed to build timestamp assignment for '{}-{}': {e}",
4506 elem.topic(),
4507 elem.partition()
4508 ))
4509 })?;
4510 }
4511 if restored != self.offsets.partition_count() {
4512 return Err(ConnectorError::ConfigurationError(
4513 "Kafka resume checkpoint references a partition absent from timestamp metadata"
4514 .into(),
4515 ));
4516 }
4517 consumer.assign(&positioned).map_err(|e| {
4518 ConnectorError::ConnectionFailed(format!(
4519 "failed to assign timestamp/checkpoint offsets: {e}"
4520 ))
4521 })?;
4522 self.manual_topic_partitions = positioned
4523 .elements()
4524 .iter()
4525 .map(|entry| (entry.topic().to_string(), entry.partition()))
4526 .collect();
4527 info!(
4528 timestamp_ms = ts_ms,
4529 partition_count = positioned.count(),
4530 restored_partitions = restored,
4531 "assigned consumer to exact checkpoint/timestamp offsets"
4532 );
4533 }
4534 }
4535 } self.prefetch_schema_registry(&kafka_config).await?;
4542
4543 self.state = ConnectorState::Running;
4544 info!("Kafka source connector started successfully");
4545 Ok(())
4546 }
4547
4548 async fn discover_schema(
4549 &mut self,
4550 properties: &std::collections::HashMap<String, String>,
4551 ) -> Result<(), ConnectorError> {
4552 let cfg = crate::config::ConnectorConfig::with_properties("kafka", properties.clone());
4553 let kafka_config = KafkaSourceConfig::from_config(&cfg)?;
4554 if kafka_config.format != Format::Avro {
4555 return Ok(());
4556 }
4557
4558 let topic = match &kafka_config.subscription {
4559 TopicSubscription::Topics(topics) => match topics.first() {
4560 Some(t) => {
4561 if topics.len() > 1 {
4562 warn!(topics = ?topics, chosen = %t,
4563 "multi-topic source: using first topic's SR schema");
4564 }
4565 t.clone()
4566 }
4567 None => return Ok(()),
4568 },
4569 TopicSubscription::Pattern(pattern) => {
4570 return Err(ConnectorError::ConfigurationError(format!(
4571 "topic.pattern '{pattern}' cannot auto-discover a schema; \
4572 declare columns explicitly"
4573 )));
4574 }
4575 };
4576
4577 let Some(sr_client) = Self::build_sr_client(&kafka_config)? else {
4578 return Ok(());
4579 };
4580
4581 let subject = resolve_value_subject(
4582 kafka_config.schema_registry_subject_strategy,
4583 kafka_config.schema_registry_record_name.as_deref(),
4584 &topic,
4585 );
4586 let timeout = kafka_config.schema_registry_discovery_timeout;
4587
4588 match tokio::time::timeout(timeout, sr_client.get_latest_schema(&subject)).await {
4589 Ok(Ok(cached)) => {
4590 self.metrics.record_sr_discovery_success();
4591 info!(%subject, schema_id = cached.id,
4592 fields = cached.arrow_schema.fields().len(),
4593 "discovered Avro schema from Schema Registry");
4594 self.schema = cached.arrow_schema;
4595 Ok(())
4596 }
4597 Ok(Err(e)) => {
4598 self.metrics.record_sr_discovery_failure();
4599 Err(e)
4600 }
4601 Err(_) => {
4602 self.metrics.record_sr_discovery_timeout();
4603 Err(ConnectorError::Timeout(
4604 u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX),
4605 ))
4606 }
4607 }
4608 }
4609
4610 async fn poll_batch(
4611 &mut self,
4612 max_records: usize,
4613 ) -> Result<Option<SourceBatch>, ConnectorError> {
4614 if self.state != ConnectorState::Running {
4615 return Err(ConnectorError::InvalidState {
4616 expected: "Running".into(),
4617 actual: self.state.to_string(),
4618 });
4619 }
4620
4621 self.ensure_reader_started();
4623 self.check_reader_health("polling source data")?;
4624
4625 let limit = max_records.min(self.config.max_poll_records);
4626
4627 self.poll_payloads.clear();
4630 self.poll_payload_buf.clear();
4631 self.poll_payload_offsets.clear();
4632 self.poll_staged_offsets.clear();
4633 self.poll_meta_partitions.clear();
4634 self.poll_meta_offsets.clear();
4635 self.poll_meta_timestamps.clear();
4636 self.poll_meta_headers.clear();
4637
4638 let mut total_bytes: u64 = 0;
4639 let include_metadata = self.config.include_metadata;
4640 let include_headers = self.config.include_headers;
4641
4642 let (drained_assignment_version, retires_rotation_baseline) = {
4646 let vnode_registry = self
4647 .vnode_assignment
4648 .as_ref()
4649 .map(|(registry, self_id)| (Arc::clone(registry), *self_id));
4650 let vnode_publication = vnode_registry
4651 .as_ref()
4652 .map(|(registry, self_id)| (registry.read_assignment(), *self_id));
4653 if let Some((published, _)) = vnode_publication.as_ref() {
4654 if self.reconciled_assignment_version.load(Ordering::Acquire) != published.version()
4655 {
4656 return Ok(None);
4657 }
4658 }
4659
4660 let rotation_baselines = if self
4664 .rotation_partition_baseline_count
4665 .load(Ordering::Acquire)
4666 == 0
4667 {
4668 None
4669 } else {
4670 Some(Arc::clone(&lock_or_recover(&self.assignment_publication)))
4671 };
4672 if let Some(publication) = rotation_baselines.as_deref() {
4673 if self.applied_rotation_baseline_version != Some(publication.assignment_version) {
4674 let mut snapshot = lock_or_recover(&self.offset_snapshot);
4675 for (topic, partitions) in &publication.baselines {
4676 for partition in partitions.keys() {
4677 self.offsets.remove(topic, *partition);
4678 snapshot.remove(topic, *partition);
4679 }
4680 }
4681 self.applied_rotation_baseline_version = Some(publication.assignment_version);
4682 }
4683 }
4684 let vnode_ownership = vnode_publication
4685 .as_ref()
4686 .map(|(published, self_id)| (published.owners(), *self_id));
4687
4688 let rx = self
4689 .msg_rx
4690 .as_mut()
4691 .ok_or_else(|| ConnectorError::InvalidState {
4692 expected: "reader initialized".into(),
4693 actual: "reader is None".into(),
4694 })?;
4695
4696 while self.poll_payloads.len() < limit {
4697 match rx.try_recv() {
4698 Ok(item) => {
4699 self.channel_len.fetch_sub(1, Ordering::Release);
4700 let kp = match item {
4701 KafkaReaderItem::Payload(payload) => payload,
4702 KafkaReaderItem::DrainBoundary(boundary) => {
4703 let Some(active) = self.source_drain.as_mut() else {
4704 self.state = ConnectorState::Failed;
4705 return Err(ConnectorError::Internal(
4706 "Kafka reader emitted a drain boundary without an active round"
4707 .into(),
4708 ));
4709 };
4710 if boundary.round != active.request.round
4711 || active.boundary.is_some()
4712 {
4713 self.state = ConnectorState::Failed;
4714 return Err(ConnectorError::Internal(
4715 "Kafka reader emitted a stale or duplicate drain boundary"
4716 .into(),
4717 ));
4718 }
4719 active.boundary = Some(boundary);
4720 break;
4724 }
4725 };
4726 let payload_is_current = vnode_payload_is_current(
4727 vnode_ownership
4728 .as_ref()
4729 .map(|(assignment, self_id)| (*assignment, *self_id)),
4730 kp.partition_vnode,
4731 rotation_baselines.as_deref().and_then(|publication| {
4732 rotation_partition_baseline(
4733 &publication.baselines,
4734 kp.topic.as_ref(),
4735 kp.partition,
4736 )
4737 }),
4738 kp.offset,
4739 )
4740 .map_err(|error| {
4741 terminalize_guaranteed_poll_error(
4742 self.delivery,
4743 &mut self.state,
4744 &self.metrics,
4745 self.reader_shutdown.as_ref(),
4746 error,
4747 )
4748 })?;
4749 if !payload_is_current {
4750 debug!(
4751 topic = kp.topic.as_ref(),
4752 partition = kp.partition,
4753 offset = kp.offset,
4754 "discarded Kafka payload outside the current vnode handoff cut"
4755 );
4756 continue;
4757 }
4758 self.poll_payloads.push(kp);
4759 }
4760 Err(crossfire::TryRecvError::Empty) => break,
4761 Err(crossfire::TryRecvError::Disconnected) => {
4762 self.state = ConnectorState::Failed;
4763 return Err(ConnectorError::Internal(
4764 "Kafka reader task exited unexpectedly".into(),
4765 ));
4766 }
4767 }
4768 }
4769 let assignment_version = vnode_publication
4770 .as_ref()
4771 .map(|(published, _)| published.version());
4772 let retires_baseline = rotation_baselines.as_deref().is_some_and(|publication| {
4773 self.poll_payloads.iter().any(|payload| {
4774 rotation_partition_baseline(
4775 &publication.baselines,
4776 payload.topic.as_ref(),
4777 payload.partition,
4778 )
4779 .is_some_and(|next| payload.offset >= next)
4780 })
4781 });
4782 (assignment_version, retires_baseline)
4783 };
4784 for kp in self.poll_payloads.drain(..) {
4788 total_bytes += kp.data.len() as u64;
4789 let start = self.poll_payload_buf.len();
4790 self.poll_payload_buf.extend_from_slice(&kp.data);
4791 self.poll_payload_offsets.push((start, kp.data.len()));
4792
4793 self.poll_staged_offsets
4796 .push((Arc::clone(&kp.topic), kp.partition, kp.offset));
4797
4798 if include_metadata {
4799 self.poll_meta_partitions.push(kp.partition);
4800 self.poll_meta_offsets.push(kp.offset);
4801 self.poll_meta_timestamps.push(kp.timestamp_ms);
4802 }
4803 if include_headers {
4804 self.poll_meta_headers.push(kp.headers_json);
4805 }
4806 }
4807
4808 let rebalance_events = self.rebalance_counter.swap(0, Ordering::Relaxed);
4810 for _ in 0..rebalance_events {
4811 self.metrics.record_rebalance();
4812 }
4813
4814 let current_revoke_gen = self.revoke_generation.load(Ordering::Acquire);
4817 let had_revoke = current_revoke_gen != self.last_seen_revoke_gen;
4818 if had_revoke {
4819 self.last_seen_revoke_gen = current_revoke_gen;
4820 let assigned = lock_or_recover(&self.rebalance_state).assignment_snapshot();
4821 let before = self.offsets.partition_count();
4822 self.offsets.retain_assigned(&assigned);
4823 lock_or_recover(&self.offset_snapshot).retain_assigned(&assigned);
4824 let after = self.offsets.partition_count();
4825 if before != after {
4826 debug!(
4827 before,
4828 after, "purged revoked partition offsets after rebalance"
4829 );
4830 }
4831 }
4832
4833 if self.poll_payload_offsets.is_empty() {
4834 return Ok(None);
4835 }
4836
4837 let (batch, good_indices) = self.decode_polled_payloads().await?;
4838
4839 let batch = self.append_metadata_columns(
4840 batch,
4841 good_indices.as_deref(),
4842 include_metadata,
4843 include_headers,
4844 )?;
4845 let output = SourceBatch::new(batch);
4849 let num_rows = output.num_rows();
4850
4851 if !self.poll_staged_offsets.is_empty() {
4852 {
4853 let mut snapshot = lock_or_recover(&self.offset_snapshot);
4854 for (topic, partition, offset) in &self.poll_staged_offsets {
4855 self.offsets.update_arc(topic, *partition, *offset);
4856 snapshot.update_arc(topic, *partition, *offset);
4857 }
4858 }
4859 if retires_rotation_baseline {
4860 if let Some(version) = drained_assignment_version {
4861 let mut published = lock_or_recover(&self.assignment_publication);
4862 if published.assignment_version == version {
4863 let publication = Arc::make_mut(&mut published);
4864 retire_accepted_rotation_baselines(
4865 &mut publication.baselines,
4866 &self.poll_staged_offsets,
4867 );
4868 let count = rotation_baselines_len(&publication.baselines);
4869 self.rotation_partition_baseline_count
4870 .store(count, Ordering::Release);
4871 }
4872 }
4873 }
4874 self.poll_staged_offsets.clear();
4875 }
4876
4877 self.metrics.record_poll(num_rows as u64, total_bytes);
4878
4879 debug!(
4880 records = num_rows,
4881 bytes = total_bytes,
4882 "polled batch from Kafka"
4883 );
4884
4885 Ok(Some(output))
4886 }
4887
4888 fn schema(&self) -> SchemaRef {
4889 self.schema.clone()
4890 }
4891
4892 fn checkpoint_ready(&self) -> Result<bool, ConnectorError> {
4893 self.check_reader_health("reconciling source ownership")?;
4894 Ok(self.vnode_assignment.as_ref().is_none_or(|(registry, _)| {
4895 let version = registry.assignment_version();
4896 version != 0 && self.reconciled_assignment_version.load(Ordering::Acquire) == version
4897 }))
4898 }
4899
4900 fn drive_control_plane(&mut self) {
4901 self.ensure_reader_started();
4902 }
4903
4904 fn checkpoint(&self) -> SourceCheckpoint {
4905 self.try_capture_checkpoint()
4906 .ok()
4907 .flatten()
4908 .unwrap_or_default()
4909 }
4910
4911 fn try_checkpoint(&self) -> Result<Option<SourceCheckpoint>, ConnectorError> {
4912 self.validate_active_drain_cursor()?;
4913 self.try_capture_checkpoint()
4914 }
4915
4916 fn data_ready_notify(&self) -> Option<Arc<Notify>> {
4917 Some(Arc::clone(&self.data_ready))
4918 }
4919
4920 async fn notify_epoch_committed(
4921 &mut self,
4922 epoch: u64,
4923 checkpoint: &SourceCheckpoint,
4924 ) -> Result<(), ConnectorError> {
4925 if !self.config.broker_commit_on_checkpoint || checkpoint.is_empty() {
4926 return Ok(());
4927 }
4928 let tpl = OffsetTracker::try_from_checkpoint(checkpoint)?.to_topic_partition_list();
4929 if tpl.count() == 0 {
4930 return Ok(());
4931 }
4932 let Some(consumer) = self.consumer.as_ref() else {
4933 self.metrics.commit_failures.inc();
4937 warn!(
4938 epoch,
4939 "Kafka progress commit skipped because the consumer is absent"
4940 );
4941 return Ok(());
4942 };
4943
4944 if let Err(error) = consumer.commit(&tpl, CommitMode::Async) {
4948 self.metrics.commit_failures.inc();
4949 warn!(epoch, %error, "Kafka progress commit was not accepted for enqueue");
4950 }
4951 Ok(())
4952 }
4953
4954 async fn close(&mut self) -> Result<(), ConnectorError> {
4955 info!("closing Kafka source connector");
4956
4957 if let Some(tx) = self.reader_shutdown.take() {
4960 let _ = tx.send(true);
4961 }
4962 if let Some(ref consumer) = self.consumer {
4965 consumer.unsubscribe();
4966 }
4967 let deadline = tokio::time::Instant::now() + KAFKA_BACKGROUND_CLOSE_BUDGET;
4968 join_background_task(&mut self.reader_handle, deadline, "reader").await;
4969 self.msg_rx = None;
4970 self.reader_drain_tx = None;
4971 self.source_drain = None;
4972 self.channel_len.store(0, Ordering::Release);
4973 if let Some(consumer) = self.consumer.take() {
4974 reap_last_arc_off_runtime(&self.blocking_tasks, consumer, deadline, "consumer").await;
4975 }
4976 if !self.blocking_tasks.join_until(deadline).await {
4977 self.blocking_tasks.ensure_reaper();
4978 }
4979 self.state = ConnectorState::Closed;
4980 info!("Kafka source connector closed");
4981 Ok(())
4982 }
4983}
4984
4985impl Drop for KafkaSource {
4986 fn drop(&mut self) {
4987 if let Some(shutdown) = self.reader_shutdown.take() {
4988 let _ = shutdown.send(true);
4989 }
4990 if let Some(consumer) = self.consumer.as_ref() {
4991 consumer.unsubscribe();
4992 }
4993 self.blocking_tasks.retire();
4994 if let Some(handle) = self.reader_handle.take() {
4995 ensure_background_task_reaper(handle, &self.task_owner, "reader");
4996 }
4997 self.blocking_tasks.ensure_reaper();
4998 if let Some(consumer) = self.consumer.take() {
4999 self.blocking_tasks.spawn_final_drop(consumer, "consumer");
5000 }
5001 }
5002}
5003
5004impl std::fmt::Debug for KafkaSource {
5005 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5006 f.debug_struct("KafkaSource")
5007 .field("state", &self.state)
5008 .field("subscription", &self.config.subscription)
5009 .field("group_id", &self.config.group_id)
5010 .field("format", &self.config.format)
5011 .field("delivery", &self.delivery)
5012 .field("partitions", &self.offsets.partition_count())
5013 .finish_non_exhaustive()
5014 }
5015}
5016
5017fn log_schema_drift(declared: &arrow_schema::Schema, live: &arrow_schema::Schema, subject: &str) {
5020 if declared.fields().is_empty() || declared.fields() == live.fields() {
5021 return;
5022 }
5023 let decl: BTreeSet<&str> = declared
5024 .fields()
5025 .iter()
5026 .map(|f| f.name().as_str())
5027 .collect();
5028 let lv: BTreeSet<&str> = live.fields().iter().map(|f| f.name().as_str()).collect();
5029 warn!(
5030 %subject,
5031 missing_in_sr = ?decl.difference(&lv).collect::<Vec<_>>(),
5032 added_in_sr = ?lv.difference(&decl).collect::<Vec<_>>(),
5033 "schema drift: re-apply CREATE SOURCE DDL to pick up the current SR schema"
5034 );
5035}
5036
5037fn select_deserializer(format: Format) -> Box<dyn RecordDeserializer> {
5038 match format {
5039 Format::Avro => Box::new(AvroDeserializer::new()),
5040 other => serde::create_deserializer(other).unwrap_or_else(|_| {
5041 warn!(format = %other, "unsupported format, falling back to JSON");
5042 Box::new(serde::json::JsonDeserializer::new())
5043 }),
5044 }
5045}
5046
5047#[cfg(test)]
5048mod tests;