1use arrow_array::builder::BinaryBuilder;
5use arrow_array::{Array, RecordBatch, RecordBatchOptions, StringArray, UInt32Array};
6use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit};
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::collections::BTreeSet;
14use std::num::NonZeroU64;
15use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, AtomicUsize, Ordering};
16use std::sync::{Arc, Mutex};
17use tokio::sync::{Mutex as AsyncMutex, Notify};
18use tracing::{debug, info, warn};
19
20use crate::checkpoint::{SourceCheckpoint, SourceCheckpointDelta};
21use crate::config::{ConnectorConfig, ConnectorState};
22use crate::connector::{
23 ConnectorTaskGuard, ConnectorTaskOwner, ConnectorTaskTracker, DeliveryGuarantee, SourceBatch,
24 SourceConnector, SourceConsistency, SourceContract, SourceDrainOutcome, SourceDrainRequest,
25 SourceDrainResolution, SourceInputMode, SourceMutation, SourcePosition,
26 SourceRowPositionCapability, SourceRowPositions, SourceStart, SourceTopology,
27};
28use crate::error::{ConnectorError, SerdeError};
29use crate::serde::{self, Format, RecordDeserializer};
30
31use super::avro::AvroDeserializer;
32use super::config::{
33 resolve_value_subject, KafkaSourceConfig, OffsetReset, SchemaEvolutionStrategy, StartupMode,
34 TopicSubscription,
35};
36use super::metadata_error::{fetch_error, invalid_response, topic_error};
37use super::metrics::KafkaSourceMetrics;
38use super::offsets::{OffsetTracker, KAFKA_PARTITION_BASELINE_PREFIX};
39use super::rebalance::RebalanceState;
40use super::schema_registry::SchemaRegistryClient;
41
42use super::rebalance::LaminarConsumerContext;
43use crate::schema::evolution::SchemaEvolution;
44use crate::schema::traits::{CompatibilityMode, EvolutionVerdict};
45
46mod background;
47mod checkpoint;
48mod debezium;
49mod decoding;
50mod drain;
51mod lifecycle;
52mod metadata;
53mod polling;
54mod reader;
55mod startup;
56
57use background::{
58 ensure_background_task_reaper, join_background_task, reap_last_arc_off_runtime,
59 KafkaBlockingTaskError, KafkaBlockingTasks,
60};
61use checkpoint::{
62 acquired_numeric_position, assignment_seek_tpl, build_vnode_assignment_tpl,
63 consumer_creation_error, decode_partition_baselines, deterministic_initial_offset,
64 kafka_input_channels, kafka_output_schema, kafka_reader_error_is_transient,
65 kafka_row_positions, retire_accepted_rotation_baselines, rotation_baselines_len,
66 rotation_partition_baseline, startup_default_offset, tpl_of, update_rotation_baselines,
67 validate_kafka_output_schema, validate_partition_baselines, validate_positions_not_expired,
68 validate_resume_input_channels, vnode_payload_is_current, NormalizedDebeziumBatch,
69};
70use debezium::normalize_kafka_debezium_batch;
71use drain::{
72 cached_partition_vnode, kafka_bootstrap_is_unassigned, kafka_drain_partitions,
73 kafka_drain_target_ready, kafka_owned_partition_sets, kafka_partition_routes,
74 kafka_partition_set, lock_or_recover, publish_reader_fault, resolve_kafka_reader_drain,
75 terminalize_guaranteed_poll_error, try_capture_at_assignment_fence, validate_kafka_assignment,
76 validate_kafka_partition_results, KafkaAssignmentPublication, KafkaDrainBoundary,
77 KafkaDrainPartition, KafkaDrainPosition, KafkaPartitionBaselines, KafkaPartitionRoutes,
78 KafkaPartitionSet, KafkaPayload, KafkaReaderDrain, KafkaReaderDrainCommand, KafkaReaderItem,
79 KafkaReaderRx, KafkaRotationBaselines, KafkaSourceDrain, KafkaStartPlan,
80 KAFKA_BACKGROUND_CLOSE_BUDGET, KAFKA_POSITION_LOOKUP_BUDGET, KAFKA_POSITION_LOOKUP_CONCURRENCY,
81};
82use metadata::{
83 fetch_explicit_topic_metadata, fetch_partition_low_watermarks, fetch_partition_watermarks,
84 resolve_timestamp_offsets,
85};
86
87pub struct KafkaSource {
105 consumer: Option<Arc<StreamConsumer<LaminarConsumerContext>>>,
106 config: KafkaSourceConfig,
107 delivery: DeliveryGuarantee,
110 deserializer: Box<dyn RecordDeserializer>,
111 offsets: OffsetTracker,
112 state: ConnectorState,
113 metrics: KafkaSourceMetrics,
114 schema: SchemaRef,
115 channel_len: Arc<AtomicUsize>,
116 rebalance_state: Arc<Mutex<RebalanceState>>,
117 rebalance_counter: Arc<AtomicU64>,
119 revoke_generation: Arc<AtomicU64>,
122 assign_generation: Arc<AtomicU64>,
125 last_seen_revoke_gen: u64,
126 schema_registry: Option<Arc<SchemaRegistryClient>>,
127 data_ready: Arc<Notify>,
128 msg_rx: Option<KafkaReaderRx>,
129 reader_handle: Option<tokio::task::JoinHandle<()>>,
130 reader_shutdown: Option<tokio::sync::watch::Sender<bool>>,
131 task_owner: ConnectorTaskOwner,
133 task_tracker: ConnectorTaskTracker,
135 blocking_tasks: KafkaBlockingTasks,
137 reader_fault: Arc<Mutex<Option<Arc<str>>>>,
139 reader_drain_tx: Option<tokio::sync::mpsc::UnboundedSender<KafkaReaderDrainCommand>>,
141 source_drain: Option<KafkaSourceDrain>,
142 offset_snapshot: Arc<Mutex<OffsetTracker>>,
145 deterministic_unrecorded_position: Arc<AtomicBool>,
151
152 source_name: Arc<str>,
154
155 vnode_assignment: Option<(
159 Arc<laminar_core::state::VnodeRegistry>,
160 laminar_core::state::NodeId,
161 )>,
162 vnode_partition_routes: KafkaPartitionRoutes,
164 manual_topic_partitions: KafkaPartitionSet,
167 manual_input_channels: Arc<[Vec<u8>]>,
169 manual_partition_baselines: KafkaPartitionBaselines,
172 assignment_publication: Arc<Mutex<Arc<KafkaAssignmentPublication>>>,
174 rotation_partition_baseline_count: Arc<AtomicUsize>,
177 applied_rotation_baseline_version: Option<u64>,
181 batch_cursor_assignment_version: Option<u64>,
183 reconciled_assignment_version: Arc<AtomicU64>,
187
188 last_avro_schema: Option<SchemaRef>,
190
191 poll_payloads: Vec<KafkaPayload>,
193 poll_payload_buf: Vec<u8>,
194 poll_payload_offsets: Vec<(usize, usize)>,
195 poll_meta_partitions: Vec<i32>,
196 poll_meta_offsets: Vec<i64>,
197 poll_meta_timestamps: Vec<Option<i64>>,
198 poll_meta_headers: Vec<Option<String>>,
199 poll_staged_offsets: Vec<(Arc<str>, i32, i64)>,
203}
204
205impl KafkaSource {
206 #[must_use]
208 pub fn new(
209 schema: SchemaRef,
210 config: KafkaSourceConfig,
211 registry: Option<&prometheus::Registry>,
212 ) -> Self {
213 Self::build_base(schema, config, select_deserializer, None, registry)
214 }
215
216 #[must_use]
218 pub fn with_schema_registry(
219 schema: SchemaRef,
220 config: KafkaSourceConfig,
221 sr_client: SchemaRegistryClient,
222 ) -> Self {
223 let sr = Arc::new(sr_client);
224 let sr_clone = Arc::clone(&sr);
225 let deser_factory = move |format: Format| -> Box<dyn RecordDeserializer> {
226 if format == Format::Avro {
227 Box::new(AvroDeserializer::with_schema_registry(sr_clone))
228 } else {
229 select_deserializer(format)
230 }
231 };
232 Self::build_base(schema, config, deser_factory, Some(sr), None)
233 }
234
235 fn build_sr_client(
238 config: &KafkaSourceConfig,
239 ) -> Result<Option<SchemaRegistryClient>, ConnectorError> {
240 let Some(sr_url) = config.schema_registry_url.as_ref() else {
241 return Ok(None);
242 };
243 let client = if let Some(ca) = config.schema_registry_ssl_ca_location.as_deref() {
244 SchemaRegistryClient::with_tls_mtls(
245 sr_url.clone(),
246 config.schema_registry_auth.clone(),
247 ca,
248 config.schema_registry_ssl_certificate_location.as_deref(),
249 config.schema_registry_ssl_key_location.as_deref(),
250 )?
251 } else {
252 SchemaRegistryClient::new(sr_url.clone(), config.schema_registry_auth.clone())?
253 };
254 Ok(Some(client))
255 }
256
257 fn build_base(
258 schema: SchemaRef,
259 config: KafkaSourceConfig,
260 deser_factory: impl FnOnce(Format) -> Box<dyn RecordDeserializer>,
261 schema_registry: Option<Arc<SchemaRegistryClient>>,
262 registry: Option<&prometheus::Registry>,
263 ) -> Self {
264 let deserializer = deser_factory(config.format);
265 let channel_len = Arc::new(AtomicUsize::new(0));
266 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
267 let blocking_guard = task_owner
268 .track()
269 .expect("new Kafka source task generation must be live");
270
271 Self {
272 consumer: None,
273 config,
274 delivery: DeliveryGuarantee::BestEffort,
275 deserializer,
276 offsets: OffsetTracker::new(),
277 state: ConnectorState::Created,
278 metrics: KafkaSourceMetrics::new(registry),
279 schema,
280 channel_len,
281 rebalance_state: Arc::new(Mutex::new(RebalanceState::new())),
282 rebalance_counter: Arc::new(AtomicU64::new(0)),
283 revoke_generation: Arc::new(AtomicU64::new(0)),
284 assign_generation: Arc::new(AtomicU64::new(0)),
285 last_seen_revoke_gen: 0,
286 schema_registry,
287 data_ready: Arc::new(Notify::new()),
288 msg_rx: None,
289 reader_handle: None,
290 reader_shutdown: None,
291 task_owner,
292 task_tracker,
293 blocking_tasks: KafkaBlockingTasks::new(blocking_guard),
294 reader_fault: Arc::new(Mutex::new(None)),
295 reader_drain_tx: None,
296 source_drain: None,
297 offset_snapshot: Arc::new(Mutex::new(OffsetTracker::new())),
298 deterministic_unrecorded_position: Arc::new(AtomicBool::new(false)),
299 source_name: Arc::from(""),
300 vnode_assignment: None,
301 vnode_partition_routes: KafkaPartitionRoutes::new(),
302 manual_topic_partitions: std::collections::HashSet::new(),
303 manual_input_channels: Arc::from([]),
304 manual_partition_baselines: std::collections::HashMap::new(),
305 assignment_publication: Arc::new(Mutex::new(Arc::new(
306 KafkaAssignmentPublication::default(),
307 ))),
308 rotation_partition_baseline_count: Arc::new(AtomicUsize::new(0)),
309 applied_rotation_baseline_version: None,
310 batch_cursor_assignment_version: None,
311 reconciled_assignment_version: Arc::new(AtomicU64::new(0)),
312 last_avro_schema: None,
313 poll_payloads: Vec::new(),
314 poll_payload_buf: Vec::new(),
315 poll_payload_offsets: Vec::new(),
316 poll_meta_partitions: Vec::new(),
317 poll_meta_offsets: Vec::new(),
318 poll_meta_timestamps: Vec::new(),
319 poll_meta_headers: Vec::new(),
320 poll_staged_offsets: Vec::new(),
321 }
322 }
323
324 #[must_use]
326 pub fn state(&self) -> ConnectorState {
327 self.state
328 }
329
330 #[must_use]
332 pub fn channel_len(&self) -> Arc<AtomicUsize> {
333 Arc::clone(&self.channel_len)
334 }
335
336 #[must_use]
338 pub fn rebalance_state(&self) -> Arc<Mutex<RebalanceState>> {
339 Arc::clone(&self.rebalance_state)
340 }
341
342 #[must_use]
344 pub fn has_schema_registry(&self) -> bool {
345 self.schema_registry.is_some()
346 }
347
348 fn fail_startup(&mut self) {
349 self.state = ConnectorState::Failed;
350 self.blocking_tasks.retire();
351 self.blocking_tasks.ensure_reaper();
352 if let Some(consumer) = self.consumer.take() {
353 consumer.unsubscribe();
354 self.blocking_tasks
355 .spawn_final_drop(consumer, "consumer after failed startup");
356 }
357 }
358
359 fn capture_drain_positions(
360 &self,
361 inputs: &[KafkaDrainPartition],
362 prepare_deadline: Option<tokio::time::Instant>,
363 ) -> Result<Arc<[KafkaDrainPosition]>, ConnectorError> {
364 if prepare_deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
365 return Err(ConnectorError::Internal(
366 "Kafka drain deadline expired before cursor capture".into(),
367 ));
368 }
369 let publication = Arc::clone(&lock_or_recover(&self.assignment_publication));
370 let mut cut = Vec::with_capacity(inputs.len());
371 for input in inputs {
372 if prepare_deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
373 return Err(ConnectorError::Internal(
374 "Kafka drain deadline expired during cursor capture".into(),
375 ));
376 }
377 let next_offset =
378 if let Some(offset) = self.offsets.get(input.topic.as_ref(), input.partition) {
379 offset.checked_add(1).ok_or_else(|| {
380 ConnectorError::Internal(format!(
381 "Kafka drain offset overflow for '{}-{}'",
382 input.topic, input.partition
383 ))
384 })?
385 } else if let Some(next) = rotation_partition_baseline(
386 &publication.baselines,
387 input.topic.as_ref(),
388 input.partition,
389 ) {
390 next
391 } else if let Some(next) = self
392 .manual_partition_baselines
393 .get(&(input.topic.to_string(), input.partition))
394 {
395 *next
396 } else {
397 return Err(ConnectorError::InvalidState {
398 expected: format!(
399 "numeric next-to-read position for drained Kafka input '{}-{}'",
400 input.topic, input.partition
401 ),
402 actual: "no accepted offset or deterministic baseline".into(),
403 });
404 };
405 cut.push(KafkaDrainPosition {
406 topic: Arc::clone(&input.topic),
407 partition: input.partition,
408 next_offset,
409 });
410 }
411 if prepare_deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
412 return Err(ConnectorError::Internal(
413 "Kafka drain deadline expired during cursor capture".into(),
414 ));
415 }
416 Ok(cut.into())
417 }
418
419 fn check_reader_health(&self, operation: &str) -> Result<(), ConnectorError> {
420 if let Some(reason) = lock_or_recover(&self.reader_fault).clone() {
421 return Err(ConnectorError::Internal(format!(
422 "Kafka reader terminated while {operation}: {reason}"
423 )));
424 }
425 if self
426 .reader_handle
427 .as_ref()
428 .is_some_and(tokio::task::JoinHandle::is_finished)
429 {
430 return Err(ConnectorError::Internal(format!(
431 "Kafka reader task exited while {operation}"
432 )));
433 }
434 Ok(())
435 }
436
437 fn validate_active_drain_cursor(&self) -> Result<(), ConnectorError> {
438 self.check_reader_health("validating a drain cursor")?;
439 let Some(active) = self.source_drain.as_ref() else {
440 return Ok(());
441 };
442 let Some(expected) = active.cut.as_deref() else {
443 return Ok(());
444 };
445 let Some(boundary) = active.boundary.as_ref() else {
446 return Err(ConnectorError::Internal(
447 "Kafka drain cut exists without its FIFO boundary".into(),
448 ));
449 };
450 let current = self.capture_drain_positions(&boundary.inputs, None)?;
453 if current.as_ref() != expected {
454 return Err(ConnectorError::Internal(
455 "Kafka input cursor advanced after its drain receipt".into(),
456 ));
457 }
458 Ok(())
459 }
460}
461
462impl Drop for KafkaSource {
463 fn drop(&mut self) {
464 if let Some(shutdown) = self.reader_shutdown.take() {
465 let _ = shutdown.send(true);
466 }
467 if let Some(consumer) = self.consumer.as_ref() {
468 consumer.unsubscribe();
469 }
470 self.blocking_tasks.retire();
471 if let Some(handle) = self.reader_handle.take() {
472 ensure_background_task_reaper(handle, &self.task_owner, "reader");
473 }
474 self.blocking_tasks.ensure_reaper();
475 if let Some(consumer) = self.consumer.take() {
476 self.blocking_tasks.spawn_final_drop(consumer, "consumer");
477 }
478 }
479}
480
481impl std::fmt::Debug for KafkaSource {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 f.debug_struct("KafkaSource")
484 .field("state", &self.state)
485 .field("subscription", &self.config.subscription)
486 .field("group_id", &self.config.group_id)
487 .field("format", &self.config.format)
488 .field("delivery", &self.delivery)
489 .field("partitions", &self.offsets.partition_count())
490 .finish_non_exhaustive()
491 }
492}
493
494fn log_schema_drift(declared: &arrow_schema::Schema, live: &arrow_schema::Schema, subject: &str) {
497 if declared.fields().is_empty() || declared.fields() == live.fields() {
498 return;
499 }
500 let decl: BTreeSet<&str> = declared
501 .fields()
502 .iter()
503 .map(|f| f.name().as_str())
504 .collect();
505 let lv: BTreeSet<&str> = live.fields().iter().map(|f| f.name().as_str()).collect();
506 warn!(
507 %subject,
508 missing_in_sr = ?decl.difference(&lv).collect::<Vec<_>>(),
509 added_in_sr = ?lv.difference(&decl).collect::<Vec<_>>(),
510 "schema drift: re-apply CREATE SOURCE DDL to pick up the current SR schema"
511 );
512}
513
514fn select_deserializer(format: Format) -> Box<dyn RecordDeserializer> {
515 match format {
516 Format::Avro => Box::new(AvroDeserializer::new()),
517 other => serde::create_deserializer(other).unwrap_or_else(|_| {
518 warn!(format = %other, "unsupported format, falling back to JSON");
519 Box::new(serde::json::JsonDeserializer::new())
520 }),
521 }
522}
523
524#[cfg(test)]
525mod tests;