1use arrow_array::builder::BinaryBuilder;
5use arrow_array::{Array, RecordBatch, RecordBatchOptions, StringArray, UInt32Array};
6use arrow_schema::{DataType, SchemaRef};
7use async_trait::async_trait;
8use rdkafka::consumer::{CommitMode, Consumer, StreamConsumer};
9use rdkafka::error::KafkaError;
10use rdkafka::message::Message;
11use rdkafka::ClientConfig;
12use rdkafka::TopicPartitionList;
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, SourceCheckpointDelta};
20use crate::config::{ConnectorConfig, ConnectorState};
21use crate::connector::{
22 ConnectorTaskGuard, ConnectorTaskOwner, ConnectorTaskTracker, DeliveryGuarantee, SourceBatch,
23 SourceConnector, SourceConsistency, SourceContract, SourceDrainOutcome, SourceDrainRequest,
24 SourceDrainResolution, SourceInputMode, SourceMutation, SourcePosition,
25 SourceRowPositionCapability, SourceRowPositions, SourceStart, SourceTopology,
26};
27use crate::error::{ConnectorError, SerdeError};
28use crate::serde::{self, Format, RecordDeserializer};
29
30use super::avro::AvroDeserializer;
31use super::config::{
32 KafkaSourceConfig, OffsetReset, SchemaEvolutionStrategy, StartupMode, 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
44mod background;
45mod checkpoint;
46mod debezium;
47mod decoding;
48mod drain;
49#[cfg(feature = "fpml")]
50pub(crate) mod fpml;
51mod initialization;
52mod lifecycle;
53mod metadata;
54mod polling;
55mod progress;
56mod reader;
57mod recency;
58mod record_metadata;
59mod startup;
60
61use record_metadata::{KafkaHeader, KafkaRowMetadata, KafkaTimestampType};
62pub use record_metadata::{KafkaMetadataColumn, KafkaMetadataColumns};
63
64use background::{
65 ensure_background_task_reaper, join_background_task, reap_last_arc_off_runtime,
66 KafkaBlockingTaskError, KafkaBlockingTasks,
67};
68use checkpoint::{
69 acquired_numeric_position, assignment_seek_tpl, build_vnode_assignment_tpl,
70 consumer_creation_error, decode_partition_baselines, deterministic_initial_offset,
71 kafka_input_channels, kafka_reader_error_is_transient, kafka_row_positions,
72 retire_accepted_rotation_baselines, rotation_baselines_len, rotation_partition_baseline,
73 startup_default_offset, tpl_of, update_rotation_baselines, validate_partition_baselines,
74 validate_positions_not_expired, validate_resume_input_channels, vnode_payload_is_current,
75 NormalizedDebeziumBatch,
76};
77use debezium::normalize_kafka_debezium_batch;
78use drain::{
79 cached_partition_vnode, kafka_bootstrap_is_unassigned, kafka_drain_partitions,
80 kafka_drain_target_ready, kafka_owned_partition_sets, kafka_partition_routes,
81 kafka_partition_set, lock_or_recover, publish_reader_fault, resolve_kafka_reader_drain,
82 terminalize_guaranteed_poll_error, try_capture_at_assignment_fence, validate_kafka_assignment,
83 validate_kafka_partition_results, KafkaAssignmentPublication, KafkaDrainBoundary,
84 KafkaDrainPartition, KafkaDrainPosition, KafkaPartitionBaselines, KafkaPartitionRoutes,
85 KafkaPartitionSet, KafkaPayload, KafkaReaderDrain, KafkaReaderDrainCommand, KafkaReaderItem,
86 KafkaReaderRx, KafkaRotationBaselines, KafkaSourceDrain, KafkaStartPlan,
87 KAFKA_BACKGROUND_CLOSE_BUDGET, KAFKA_POSITION_LOOKUP_BUDGET, KAFKA_POSITION_LOOKUP_CONCURRENCY,
88};
89use metadata::{
90 fetch_explicit_topic_metadata, fetch_partition_low_watermarks, fetch_partition_watermarks,
91 resolve_timestamp_offsets,
92};
93use progress::KafkaProgress;
94
95pub struct KafkaSource {
113 consumer: Option<Arc<StreamConsumer<LaminarConsumerContext>>>,
114 config: KafkaSourceConfig,
115 delivery: DeliveryGuarantee,
118 deserializer: Box<dyn RecordDeserializer>,
119 #[cfg(feature = "fpml")]
121 fpml: Option<Arc<crate::fpml::FpmlDecoder>>,
122 offsets: OffsetTracker,
123 state: ConnectorState,
124 metrics: KafkaSourceMetrics,
125 metrics_registry: Option<prometheus::Registry>,
126 progress: Option<KafkaProgress>,
127 schema: SchemaRef,
128 channel_len: Arc<AtomicUsize>,
129 rebalance_state: Arc<Mutex<RebalanceState>>,
130 rebalance_counter: Arc<AtomicU64>,
132 revoke_generation: Arc<AtomicU64>,
135 assign_generation: Arc<AtomicU64>,
138 last_seen_revoke_gen: u64,
139 schema_registry: Option<Arc<SchemaRegistryClient>>,
140 data_ready: Arc<Notify>,
141 msg_rx: Option<KafkaReaderRx>,
142 reader_handle: Option<tokio::task::JoinHandle<()>>,
143 reader_shutdown: Option<tokio::sync::watch::Sender<bool>>,
144 task_owner: ConnectorTaskOwner,
146 task_tracker: ConnectorTaskTracker,
148 blocking_tasks: KafkaBlockingTasks,
150 reader_fault: Arc<Mutex<Option<Arc<str>>>>,
152 reader_drain_tx: Option<tokio::sync::mpsc::UnboundedSender<KafkaReaderDrainCommand>>,
154 source_drain: Option<KafkaSourceDrain>,
155 offset_snapshot: Arc<Mutex<OffsetTracker>>,
158 deterministic_unrecorded_position: Arc<AtomicBool>,
164
165 source_name: Arc<str>,
167
168 vnode_assignment: Option<(
172 Arc<laminar_core::state::VnodeRegistry>,
173 laminar_core::state::NodeId,
174 )>,
175 vnode_partition_routes: KafkaPartitionRoutes,
177 manual_topic_partitions: KafkaPartitionSet,
180 manual_input_channels: Arc<[Vec<u8>]>,
182 manual_partition_baselines: KafkaPartitionBaselines,
185 assignment_publication: Arc<Mutex<Arc<KafkaAssignmentPublication>>>,
187 rotation_partition_baseline_count: Arc<AtomicUsize>,
190 applied_rotation_baseline_version: Option<u64>,
194 batch_cursor_assignment_version: Option<u64>,
196 reconciled_assignment_version: Arc<AtomicU64>,
200
201 last_avro_schema: Option<SchemaRef>,
203
204 poll_payloads: Vec<KafkaPayload>,
206 poll_payload_buf: Vec<u8>,
207 poll_payload_offsets: Vec<(usize, usize)>,
208 poll_row_metadata: Vec<KafkaRowMetadata>,
210 poll_staged_offsets: Vec<(Arc<str>, i32, i64)>,
214 poll_progress_offsets: Vec<(Arc<str>, i32, i64)>,
217}
218
219impl KafkaSource {
220 #[must_use]
225 pub fn new(
226 schema: SchemaRef,
227 config: KafkaSourceConfig,
228 registry: Option<&prometheus::Registry>,
229 ) -> Self {
230 Self::build_base(schema, config, select_deserializer, None, registry)
231 }
232
233 #[must_use]
235 pub fn with_schema_registry(
236 schema: SchemaRef,
237 config: KafkaSourceConfig,
238 sr_client: SchemaRegistryClient,
239 ) -> Self {
240 let sr = Arc::new(sr_client);
241 let sr_clone = Arc::clone(&sr);
242 let deser_factory = move |format: Format| -> Box<dyn RecordDeserializer> {
243 if format == Format::Avro {
244 Box::new(AvroDeserializer::with_schema_registry(sr_clone))
245 } else {
246 select_deserializer(format)
247 }
248 };
249 Self::build_base(schema, config, deser_factory, Some(sr), None)
250 }
251
252 fn build_sr_client(
255 config: &KafkaSourceConfig,
256 ) -> Result<Option<SchemaRegistryClient>, ConnectorError> {
257 let Some(sr_url) = config.schema_registry_url.as_ref() else {
258 return Ok(None);
259 };
260 let ca = config.schema_registry_ssl_ca_location.as_deref();
261 let certificate = config.schema_registry_ssl_certificate_location.as_deref();
262 let key = config.schema_registry_ssl_key_location.as_deref();
263 let client = if ca.is_some() || certificate.is_some() || key.is_some() {
264 SchemaRegistryClient::with_tls_mtls(
265 sr_url.clone(),
266 config.schema_registry_auth.clone(),
267 ca,
268 certificate,
269 key,
270 )?
271 } else {
272 SchemaRegistryClient::new(sr_url.clone(), config.schema_registry_auth.clone())?
273 };
274 Ok(Some(client))
275 }
276
277 fn build_base(
278 schema: SchemaRef,
279 config: KafkaSourceConfig,
280 deser_factory: impl FnOnce(Format) -> Box<dyn RecordDeserializer>,
281 schema_registry: Option<Arc<SchemaRegistryClient>>,
282 registry: Option<&prometheus::Registry>,
283 ) -> Self {
284 let deserializer = deser_factory(config.format);
285 let channel_len = Arc::new(AtomicUsize::new(0));
286 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
287 let blocking_guard = task_owner
288 .track()
289 .expect("new Kafka source task generation must be live");
290
291 Self {
292 consumer: None,
293 config,
294 delivery: DeliveryGuarantee::BestEffort,
295 deserializer,
296 offsets: OffsetTracker::new(),
297 state: ConnectorState::Created,
298 metrics: KafkaSourceMetrics::new(registry),
299 metrics_registry: registry.cloned(),
300 progress: None,
301 schema,
302 channel_len,
303 rebalance_state: Arc::new(Mutex::new(RebalanceState::new())),
304 rebalance_counter: Arc::new(AtomicU64::new(0)),
305 revoke_generation: Arc::new(AtomicU64::new(0)),
306 assign_generation: Arc::new(AtomicU64::new(0)),
307 last_seen_revoke_gen: 0,
308 schema_registry,
309 data_ready: Arc::new(Notify::new()),
310 msg_rx: None,
311 reader_handle: None,
312 reader_shutdown: None,
313 task_owner,
314 task_tracker,
315 blocking_tasks: KafkaBlockingTasks::new(blocking_guard),
316 reader_fault: Arc::new(Mutex::new(None)),
317 reader_drain_tx: None,
318 source_drain: None,
319 offset_snapshot: Arc::new(Mutex::new(OffsetTracker::new())),
320 deterministic_unrecorded_position: Arc::new(AtomicBool::new(false)),
321 source_name: Arc::from(""),
322 #[cfg(feature = "fpml")]
323 fpml: None,
324 vnode_assignment: None,
325 vnode_partition_routes: KafkaPartitionRoutes::new(),
326 manual_topic_partitions: std::collections::HashSet::new(),
327 manual_input_channels: Arc::from([]),
328 manual_partition_baselines: std::collections::HashMap::new(),
329 assignment_publication: Arc::new(Mutex::new(Arc::new(
330 KafkaAssignmentPublication::default(),
331 ))),
332 rotation_partition_baseline_count: Arc::new(AtomicUsize::new(0)),
333 applied_rotation_baseline_version: None,
334 batch_cursor_assignment_version: None,
335 reconciled_assignment_version: Arc::new(AtomicU64::new(0)),
336 last_avro_schema: None,
337 poll_payloads: Vec::new(),
338 poll_payload_buf: Vec::new(),
339 poll_payload_offsets: Vec::new(),
340 poll_row_metadata: Vec::new(),
341 poll_staged_offsets: Vec::new(),
342 poll_progress_offsets: Vec::new(),
343 }
344 }
345
346 #[must_use]
348 pub fn state(&self) -> ConnectorState {
349 self.state
350 }
351
352 #[must_use]
354 pub fn channel_len(&self) -> Arc<AtomicUsize> {
355 Arc::clone(&self.channel_len)
356 }
357
358 #[must_use]
360 pub fn rebalance_state(&self) -> Arc<Mutex<RebalanceState>> {
361 Arc::clone(&self.rebalance_state)
362 }
363
364 #[must_use]
366 pub fn has_schema_registry(&self) -> bool {
367 self.schema_registry.is_some()
368 }
369
370 fn fail_startup(&mut self) {
371 self.state = ConnectorState::Failed;
372 self.progress = None;
373 self.blocking_tasks.retire();
374 self.blocking_tasks.ensure_reaper();
375 if let Some(consumer) = self.consumer.take() {
376 consumer.unsubscribe();
377 self.blocking_tasks
378 .spawn_final_drop(consumer, "consumer after failed startup");
379 }
380 }
381
382 fn capture_drain_positions(
383 &self,
384 inputs: &[KafkaDrainPartition],
385 prepare_deadline: Option<tokio::time::Instant>,
386 ) -> Result<Arc<[KafkaDrainPosition]>, ConnectorError> {
387 if prepare_deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
388 return Err(ConnectorError::Internal(
389 "Kafka drain deadline expired before cursor capture".into(),
390 ));
391 }
392 let publication = Arc::clone(&lock_or_recover(&self.assignment_publication));
393 let mut cut = Vec::with_capacity(inputs.len());
394 for input in inputs {
395 if prepare_deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
396 return Err(ConnectorError::Internal(
397 "Kafka drain deadline expired during cursor capture".into(),
398 ));
399 }
400 let next_offset =
401 if let Some(offset) = self.offsets.get(input.topic.as_ref(), input.partition) {
402 offset.checked_add(1).ok_or_else(|| {
403 ConnectorError::Internal(format!(
404 "Kafka drain offset overflow for '{}-{}'",
405 input.topic, input.partition
406 ))
407 })?
408 } else if let Some(next) = rotation_partition_baseline(
409 &publication.baselines,
410 input.topic.as_ref(),
411 input.partition,
412 ) {
413 next
414 } else if let Some(next) = self
415 .manual_partition_baselines
416 .get(&(input.topic.to_string(), input.partition))
417 {
418 *next
419 } else {
420 return Err(ConnectorError::InvalidState {
421 expected: format!(
422 "numeric next-to-read position for drained Kafka input '{}-{}'",
423 input.topic, input.partition
424 ),
425 actual: "no accepted offset or deterministic baseline".into(),
426 });
427 };
428 cut.push(KafkaDrainPosition {
429 topic: Arc::clone(&input.topic),
430 partition: input.partition,
431 next_offset,
432 });
433 }
434 if prepare_deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
435 return Err(ConnectorError::Internal(
436 "Kafka drain deadline expired during cursor capture".into(),
437 ));
438 }
439 Ok(cut.into())
440 }
441
442 fn check_reader_health(&self, operation: &str) -> Result<(), ConnectorError> {
443 if let Some(reason) = lock_or_recover(&self.reader_fault).clone() {
444 return Err(ConnectorError::Internal(format!(
445 "Kafka reader terminated while {operation}: {reason}"
446 )));
447 }
448 if self
449 .reader_handle
450 .as_ref()
451 .is_some_and(tokio::task::JoinHandle::is_finished)
452 {
453 return Err(ConnectorError::Internal(format!(
454 "Kafka reader task exited while {operation}"
455 )));
456 }
457 Ok(())
458 }
459
460 fn validate_active_drain_cursor(&self) -> Result<(), ConnectorError> {
461 self.check_reader_health("validating a drain cursor")?;
462 let Some(active) = self.source_drain.as_ref() else {
463 return Ok(());
464 };
465 let Some(expected) = active.cut.as_deref() else {
466 return Ok(());
467 };
468 let Some(boundary) = active.boundary.as_ref() else {
469 return Err(ConnectorError::Internal(
470 "Kafka drain cut exists without its FIFO boundary".into(),
471 ));
472 };
473 let current = self.capture_drain_positions(&boundary.inputs, None)?;
476 if current.as_ref() != expected {
477 return Err(ConnectorError::Internal(
478 "Kafka input cursor advanced after its drain receipt".into(),
479 ));
480 }
481 Ok(())
482 }
483}
484
485impl Drop for KafkaSource {
486 fn drop(&mut self) {
487 self.progress = None;
488 if let Some(shutdown) = self.reader_shutdown.take() {
489 let _ = shutdown.send(true);
490 }
491 if let Some(consumer) = self.consumer.as_ref() {
492 consumer.unsubscribe();
493 }
494 self.blocking_tasks.retire();
495 if let Some(handle) = self.reader_handle.take() {
496 ensure_background_task_reaper(handle, &self.task_owner, "reader");
497 }
498 self.blocking_tasks.ensure_reaper();
499 if let Some(consumer) = self.consumer.take() {
500 self.blocking_tasks.spawn_final_drop(consumer, "consumer");
501 }
502 }
503}
504
505impl std::fmt::Debug for KafkaSource {
506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507 f.debug_struct("KafkaSource")
508 .field("state", &self.state)
509 .field("subscription", &self.config.subscription)
510 .field("group_id", &self.config.group_id)
511 .field("format", &self.config.format)
512 .field("delivery", &self.delivery)
513 .field("partitions", &self.offsets.partition_count())
514 .finish_non_exhaustive()
515 }
516}
517
518struct FpmlPending;
521
522impl RecordDeserializer for FpmlPending {
523 fn deserialize(&self, _: &[u8], _: &SchemaRef) -> Result<RecordBatch, SerdeError> {
524 Err(SerdeError::UnsupportedFormat(
525 "FpML messages decode through the FpML decoder, which starts with the source".into(),
526 ))
527 }
528
529 fn format(&self) -> Format {
530 Format::Fpml
531 }
532}
533
534fn select_deserializer(format: Format) -> Box<dyn RecordDeserializer> {
535 match format {
536 Format::Avro => Box::new(AvroDeserializer::new()),
537 Format::Fpml => Box::new(FpmlPending),
538 other => serde::create_deserializer(other).unwrap_or_else(|_| {
539 warn!(format = %other, "unsupported format, falling back to JSON");
540 Box::new(serde::json::JsonDeserializer::new())
541 }),
542 }
543}
544
545#[cfg(test)]
546mod tests;