laminar_connectors/kafka/source/
lifecycle.rs1use super::{
4 async_trait, info, join_background_task, reap_last_arc_off_runtime, warn, Arc, CommitMode,
5 ConnectorConfig, ConnectorError, ConnectorState, ConnectorTaskTracker, Consumer, Format,
6 KafkaReaderDrainCommand, KafkaSource, KafkaSourceConfig, KafkaSourceDrain, Notify,
7 OffsetTracker, Ordering, SchemaRef, SourceBatch, SourceCheckpoint, SourceConnector,
8 SourceConsistency, SourceContract, SourceDrainRequest, SourceDrainResolution, SourceInputMode,
9 SourceRowPositionCapability, SourceStart, SourceTopology, KAFKA_BACKGROUND_CLOSE_BUDGET,
10};
11
12#[async_trait]
13impl SourceConnector for KafkaSource {
14 fn supports_initialized_start(&self) -> bool {
15 true
16 }
17
18 async fn resolve_initial_position(
19 &mut self,
20 config: &ConnectorConfig,
21 ) -> Result<SourceCheckpoint, ConnectorError> {
22 self.resolve_initial_position_inner(config).await
23 }
24
25 async fn validate_initial_position(
26 &mut self,
27 config: &ConnectorConfig,
28 checkpoint: &SourceCheckpoint,
29 ) -> Result<(), ConnectorError> {
30 self.inspect_initial_position_inner(config, Some(checkpoint))
31 .await
32 .map(|_| ())
33 }
34
35 fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
36 Some(self.task_tracker.clone())
37 }
38
39 fn recency(
40 &self,
41 config: &ConnectorConfig,
42 ) -> Result<Option<Arc<dyn crate::connector::SourceRecency>>, ConnectorError> {
43 Ok(super::recency::kafka_recency(
44 &KafkaSourceConfig::from_config(config)?,
45 ))
46 }
47
48 fn contract(&self, config: &ConnectorConfig) -> Result<SourceContract, ConnectorError> {
49 let format = if config.properties().is_empty() {
50 self.config.validate()?;
51 self.config.format
52 } else {
53 KafkaSourceConfig::from_config(config)?.format
54 };
55 let input_mode = if format == Format::Debezium {
56 SourceInputMode::KeyedUpsert
57 } else {
58 SourceInputMode::AppendOnly
59 };
60 Ok(SourceContract::new(
61 SourceConsistency::Replayable,
62 SourceTopology::Splittable,
63 input_mode,
64 )
65 .with_row_positions(SourceRowPositionCapability::OrderedDeterministic)
66 .with_exact_delivery_certification())
67 }
68
69 fn set_vnode_assignment(
70 &mut self,
71 source_identity: &str,
72 registry: Arc<laminar_core::state::VnodeRegistry>,
73 self_id: laminar_core::state::NodeId,
74 ) -> Result<(), ConnectorError> {
75 if source_identity.is_empty() {
76 return Err(ConnectorError::ConfigurationError(
77 "Kafka vnode assignment requires a non-empty canonical source identity".into(),
78 ));
79 }
80 if self_id.is_unassigned() {
81 return Err(ConnectorError::ConfigurationError(
82 "Kafka vnode assignment requires a nonzero node identity".into(),
83 ));
84 }
85 info!(
86 source = source_identity,
87 self_id = self_id.0,
88 vnode_count = registry.vnode_count(),
89 "Kafka source: engine-controlled partition-to-vnode assignment enabled"
90 );
91 self.source_name = Arc::from(source_identity);
92 self.vnode_assignment = Some((registry, self_id));
93 Ok(())
94 }
95
96 fn begin_drain(
97 &mut self,
98 request: &SourceDrainRequest,
99 deadline: tokio::time::Instant,
100 ) -> Result<(), ConnectorError> {
101 self.check_reader_health("starting a global source drain")?;
102 if let Some(active) = self.source_drain.as_ref() {
103 if active.request != *request {
104 return Err(ConnectorError::InvalidState {
105 expected: format!("active Kafka drain {:?}", active.request.round),
106 actual: format!("conflicting Kafka drain {:?}", request.round),
107 });
108 }
109 return Ok(());
112 }
113 if tokio::time::Instant::now() >= deadline {
114 return Err(ConnectorError::Internal(
115 "Kafka source drain began after its engine deadline".into(),
116 ));
117 }
118 let Some((registry, _)) = self.vnode_assignment.as_ref() else {
119 return Err(ConnectorError::InvalidState {
120 expected: "Kafka cluster assignment installed before source drain".into(),
121 actual: "embedded/single Kafka source".into(),
122 });
123 };
124 if registry.assignment_version() != request.round.predecessor_version {
125 return Err(ConnectorError::InvalidState {
126 expected: format!(
127 "Kafka predecessor assignment {}",
128 request.round.predecessor_version
129 ),
130 actual: registry.assignment_version().to_string(),
131 });
132 }
133 let reconciled = self.reconciled_assignment_version.load(Ordering::Acquire);
134 if reconciled != request.round.predecessor_version {
135 return Err(ConnectorError::InvalidState {
136 expected: format!(
137 "reconciled Kafka predecessor assignment {}",
138 request.round.predecessor_version
139 ),
140 actual: reconciled.to_string(),
141 });
142 }
143 self.ensure_reader_started();
144 let tx = self.reader_drain_tx.as_ref().ok_or_else(|| {
145 ConnectorError::Internal("Kafka cluster reader has no drain control channel".into())
146 })?;
147 tx.send(KafkaReaderDrainCommand::Begin {
148 request: request.clone(),
149 deadline,
150 })
151 .map_err(|_| ConnectorError::Internal("Kafka reader drain channel closed".into()))?;
152 self.source_drain = Some(KafkaSourceDrain {
153 request: request.clone(),
154 prepare_deadline: deadline,
155 boundary: None,
156 cut: None,
157 pending_resolution: None,
158 });
159 self.data_ready.notify_one();
160 Ok(())
161 }
162
163 fn poll_drain_ready(
164 &mut self,
165 round: laminar_core::checkpoint::AssignmentDrainId,
166 ) -> Result<bool, ConnectorError> {
167 self.check_reader_health("capturing a global source drain cut")?;
168 let Some(active) = self.source_drain.as_ref() else {
169 return Err(ConnectorError::InvalidState {
170 expected: format!("active Kafka drain {round:?}"),
171 actual: "no Kafka drain".into(),
172 });
173 };
174 if active.request.round != round {
175 return Err(ConnectorError::InvalidState {
176 expected: format!("active Kafka drain {:?}", active.request.round),
177 actual: format!("cut requested for {round:?}"),
178 });
179 }
180 if active.cut.is_some() {
181 return Ok(true);
182 }
183 if tokio::time::Instant::now() >= active.prepare_deadline {
184 return Err(ConnectorError::Internal(
185 "Kafka drain deadline expired before cursor capture".into(),
186 ));
187 }
188 let Some(boundary) = active.boundary.clone() else {
189 return Ok(false);
190 };
191 let cut = self.capture_drain_positions(&boundary.inputs, Some(active.prepare_deadline))?;
192 let active = self.source_drain.as_mut().expect("checked above");
193 active.cut = Some(cut);
194 Ok(true)
195 }
196
197 async fn finish_drain(
198 &mut self,
199 resolution: SourceDrainResolution,
200 deadline: tokio::time::Instant,
201 ) -> Result<(), ConnectorError> {
202 self.finish_drain_inner(resolution, deadline).await
203 }
204
205 async fn start(&mut self, request: SourceStart) -> Result<(), ConnectorError> {
206 if self.state != ConnectorState::Created {
207 return Err(ConnectorError::InvalidState {
208 expected: ConnectorState::Created.to_string(),
209 actual: self.state.to_string(),
210 });
211 }
212 let result = self.start_with_contract(request).await;
213 if result.is_err() && self.state == ConnectorState::Initializing {
214 self.fail_startup();
215 }
216 result
217 }
218 async fn resolve_schema(
219 &mut self,
220 config: &crate::config::ConnectorConfig,
221 explicit: Option<arrow_schema::SchemaRef>,
222 ) -> Result<crate::schema::resolution::SchemaBinding, ConnectorError> {
223 super::super::schema_resolution::resolve_source(config, explicit).await
224 }
225
226 async fn discover_schema(
227 &mut self,
228 properties: &std::collections::HashMap<String, String>,
229 ) -> Result<(), ConnectorError> {
230 let config = crate::config::ConnectorConfig::with_properties("kafka", properties.clone());
231 let binding = super::super::schema_resolution::resolve_source(&config, None).await?;
232 let parsed = KafkaSourceConfig::from_config(&config)?;
233 self.schema = super::super::schema_resolution::payload_schema(&binding, &parsed.metadata)?;
234 Ok(())
235 }
236
237 async fn poll_batch(
238 &mut self,
239 max_records: usize,
240 ) -> Result<Option<SourceBatch>, ConnectorError> {
241 self.poll_batch_inner(max_records).await
242 }
243
244 fn schema(&self) -> SchemaRef {
245 self.config.metadata.output_schema(&self.schema)
246 }
247
248 fn checkpoint_ready(&self) -> Result<bool, ConnectorError> {
249 self.check_reader_health("reconciling source ownership")?;
250 Ok(self.vnode_assignment.as_ref().is_none_or(|(registry, _)| {
251 let version = registry.assignment_version();
252 version != 0 && self.reconciled_assignment_version.load(Ordering::Acquire) == version
253 }))
254 }
255
256 fn drive_control_plane(&mut self) {
257 self.ensure_reader_started();
258 }
259
260 fn checkpoint(&self) -> SourceCheckpoint {
261 self.try_capture_checkpoint()
262 .ok()
263 .flatten()
264 .unwrap_or_default()
265 }
266
267 fn try_checkpoint(&self) -> Result<Option<SourceCheckpoint>, ConnectorError> {
268 self.validate_active_drain_cursor()?;
269 self.try_capture_checkpoint()
270 }
271
272 fn data_ready_notify(&self) -> Option<Arc<Notify>> {
273 Some(Arc::clone(&self.data_ready))
274 }
275
276 async fn notify_epoch_committed(
277 &mut self,
278 epoch: u64,
279 checkpoint: &SourceCheckpoint,
280 ) -> Result<(), ConnectorError> {
281 if !self.config.broker_commit_on_checkpoint || checkpoint.is_empty() {
282 return Ok(());
283 }
284 let tpl = OffsetTracker::try_from_checkpoint(checkpoint)?.to_topic_partition_list();
285 if tpl.count() == 0 {
286 return Ok(());
287 }
288 let Some(consumer) = self.consumer.as_ref() else {
289 self.metrics.commit_failures.inc();
293 warn!(
294 epoch,
295 "Kafka progress commit skipped because the consumer is absent"
296 );
297 return Ok(());
298 };
299
300 if let Err(error) = consumer.commit(&tpl, CommitMode::Async) {
304 self.metrics.commit_failures.inc();
305 warn!(epoch, %error, "Kafka progress commit was not accepted for enqueue");
306 }
307 Ok(())
308 }
309
310 async fn close(&mut self) -> Result<(), ConnectorError> {
311 info!("closing Kafka source connector");
312
313 if let Some(tx) = self.reader_shutdown.take() {
316 let _ = tx.send(true);
317 }
318 if let Some(progress) = &mut self.progress {
319 progress.stop();
320 }
321 if let Some(ref consumer) = self.consumer {
324 consumer.unsubscribe();
325 }
326 let deadline = tokio::time::Instant::now() + KAFKA_BACKGROUND_CLOSE_BUDGET;
327 join_background_task(&mut self.reader_handle, deadline, "reader").await;
328 if let Some(mut progress) = self.progress.take() {
329 progress.close(deadline).await;
330 }
331 self.msg_rx = None;
332 self.reader_drain_tx = None;
333 self.source_drain = None;
334 self.channel_len.store(0, Ordering::Release);
335 if let Some(consumer) = self.consumer.take() {
336 reap_last_arc_off_runtime(&self.blocking_tasks, consumer, deadline, "consumer").await;
337 }
338 if !self.blocking_tasks.join_until(deadline).await {
339 self.blocking_tasks.ensure_reaper();
340 }
341 self.state = ConnectorState::Closed;
342 info!("Kafka source connector closed");
343 Ok(())
344 }
345}