laminar_connectors/kafka/avro/
mod.rs1use std::collections::{HashSet, VecDeque};
9use std::sync::Arc;
10
11use arrow_array::RecordBatch;
12use arrow_avro::reader::{Decoder, ReaderBuilder};
13use arrow_avro::schema::{AvroSchema, Fingerprint, FingerprintAlgorithm, SchemaStore};
14use arrow_schema::SchemaRef;
15use parking_lot::Mutex;
16
17use crate::error::{ConnectorError, SerdeError};
18use crate::kafka::schema_registry::SchemaRegistryClient;
19use crate::serde::{Format, RecordDeserializer};
20
21const DECODER_BATCH_CAPACITY: usize = 8192;
22
23const WRITER_SCHEMA_CAPACITY: usize = 64;
26
27const CONFLUENT_MAGIC: u8 = 0x00;
29
30const CONFLUENT_HEADER_SIZE: usize = 5;
32
33pub struct AvroDeserializer {
39 schema_store: SchemaStore,
41 schema_registry: Option<Arc<SchemaRegistryClient>>,
43 writers: VecDeque<(i32, AvroSchema)>,
45 batch_ids: HashSet<i32>,
47 decoder: Mutex<Option<Decoder>>,
49 reader_schema: Option<AvroSchema>,
50 projection: Option<Vec<usize>>,
51 schema_metrics: Option<super::KafkaSourceMetrics>,
52}
53
54impl AvroDeserializer {
55 #[must_use]
59 pub fn new() -> Self {
60 Self {
61 schema_store: SchemaStore::new_with_type(FingerprintAlgorithm::Id),
62 schema_registry: None,
63 writers: VecDeque::new(),
64 batch_ids: HashSet::new(),
65 decoder: Mutex::new(None),
66 reader_schema: None,
67 projection: None,
68 schema_metrics: None,
69 }
70 }
71
72 #[must_use]
77 pub fn with_schema_registry(registry: Arc<SchemaRegistryClient>) -> Self {
78 Self {
79 schema_store: SchemaStore::new_with_type(FingerprintAlgorithm::Id),
80 schema_registry: Some(registry),
81 writers: VecDeque::new(),
82 batch_ids: HashSet::new(),
83 decoder: Mutex::new(None),
84 reader_schema: None,
85 projection: None,
86 schema_metrics: None,
87 }
88 }
89
90 pub fn bind_reader(
95 &mut self,
96 binding: &crate::schema::resolution::SchemaBinding,
97 logical: &SchemaRef,
98 ) -> Result<(), ConnectorError> {
99 let native = binding.value.as_ref().ok_or_else(|| {
100 ConnectorError::SchemaMismatch("Avro reader has no native schema".into())
101 })?;
102 let resolved = native
103 .definition
104 .get("resolved")
105 .ok_or_else(|| {
106 ConnectorError::SchemaMismatch("Avro reader has no resolved native schema".into())
107 })?
108 .to_string();
109 let external = crate::kafka::schema_registry::avro_to_arrow_schema(&resolved)?;
110 let projection = logical
111 .fields()
112 .iter()
113 .map(|field| {
114 external.index_of(field.name()).map_err(|_| {
115 ConnectorError::SchemaMismatch(format!(
116 "reader field '{}' is absent from the native record",
117 field.name()
118 ))
119 })
120 })
121 .collect::<Result<Vec<_>, _>>()?;
122 let id = crate::kafka::schema_resolution::contract_schema_id(binding)?;
123 let id = i32::try_from(id).map_err(|_| {
124 ConnectorError::SchemaMismatch("Avro schema ID exceeds the supported wire range".into())
125 })?;
126 self.register_schema(id, &resolved)
127 .map_err(ConnectorError::Serde)?;
128 self.reader_schema = Some(AvroSchema::new(resolved));
129 self.projection = Some(projection);
130 Ok(())
131 }
132
133 pub(crate) fn set_schema_metrics(&mut self, metrics: super::KafkaSourceMetrics) {
134 self.schema_metrics = Some(metrics);
135 }
136
137 #[allow(clippy::cast_sign_loss)]
143 pub fn register_schema(
144 &mut self,
145 schema_id: i32,
146 avro_schema_json: &str,
147 ) -> Result<(), SerdeError> {
148 if schema_id <= 0 || avro_schema_json.len() > 1024 * 1024 {
149 return Err(SerdeError::MalformedInput(
150 "invalid schema ID or schema exceeds 1 MiB".into(),
151 ));
152 }
153 if self.touch(schema_id) {
154 return Ok(());
155 }
156 if self.writers.len() >= WRITER_SCHEMA_CAPACITY {
157 self.evict_one()?;
158 }
159 let avro_schema = AvroSchema::new(avro_schema_json.to_string());
160 let fp = Fingerprint::Id(schema_id as u32);
163 self.schema_store
164 .set(fp, avro_schema.clone())
165 .map_err(|e| SerdeError::MalformedInput(format!("failed to register schema: {e}")))?;
166 self.writers.push_back((schema_id, avro_schema));
167 *self.decoder.lock() = None;
170 Ok(())
171 }
172
173 #[allow(clippy::cast_sign_loss)]
176 fn evict_one(&mut self) -> Result<(), SerdeError> {
177 if self.schema_registry.is_none() {
178 return Err(SerdeError::MalformedInput(format!(
179 "at most {WRITER_SCHEMA_CAPACITY} writer schemas can be registered without a \
180 Schema Registry to fetch evicted ones"
181 )));
182 }
183 let position = self
184 .writers
185 .iter()
186 .position(|(id, _)| !self.batch_ids.contains(id))
187 .ok_or_else(|| {
188 SerdeError::MalformedInput(format!(
189 "one poll references more than {WRITER_SCHEMA_CAPACITY} Avro writer \
190 schemas; lower max.poll.records"
191 ))
192 })?;
193 self.writers.remove(position);
194 let mut store = SchemaStore::new_with_type(FingerprintAlgorithm::Id);
195 for (id, schema) in &self.writers {
196 store
197 .set(Fingerprint::Id(*id as u32), schema.clone())
198 .map_err(|e| SerdeError::MalformedInput(format!("rebuild schema store: {e}")))?;
199 }
200 self.schema_store = store;
201 Ok(())
202 }
203
204 fn touch(&mut self, schema_id: i32) -> bool {
206 let Some(position) = self.writers.iter().position(|(id, _)| *id == schema_id) else {
207 return false;
208 };
209 if let Some(entry) = self.writers.remove(position) {
210 self.writers.push_back(entry);
211 }
212 true
213 }
214
215 pub(crate) fn begin_batch(&mut self) {
217 self.batch_ids.clear();
218 }
219
220 #[must_use]
222 pub fn is_registered(&self, schema_id: i32) -> bool {
223 self.writers.iter().any(|(id, _)| *id == schema_id)
224 }
225
226 pub async fn ensure_schema_registered(
239 &mut self,
240 schema_id: i32,
241 ) -> Result<bool, ConnectorError> {
242 self.batch_ids.insert(schema_id);
243 if self.touch(schema_id) {
244 return Ok(false);
245 }
246
247 let registry = self.schema_registry.as_ref().ok_or(ConnectorError::Serde(
248 SerdeError::SchemaNotFound { schema_id },
249 ))?;
250
251 let mut observation = WriterFetchObservation::new(self.schema_metrics.clone());
252 let cached = registry.resolve_confluent_id(schema_id).await?;
253 observation.succeeded = true;
254
255 self.register_schema(schema_id, &cached.resolved_schema_str)
256 .map_err(ConnectorError::Serde)?;
257 Ok(true)
258 }
259
260 #[must_use]
264 pub fn extract_confluent_id(data: &[u8]) -> Option<i32> {
265 if data.len() < CONFLUENT_HEADER_SIZE || data[0] != CONFLUENT_MAGIC {
266 return None;
267 }
268 let id = i32::from_be_bytes([data[1], data[2], data[3], data[4]]);
269 Some(id)
270 }
271}
272
273impl Default for AvroDeserializer {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279impl RecordDeserializer for AvroDeserializer {
280 fn deserialize(&self, data: &[u8], schema: &SchemaRef) -> Result<RecordBatch, SerdeError> {
281 self.deserialize_batch(&[data], schema)
282 }
283
284 fn deserialize_batch(
285 &self,
286 records: &[&[u8]],
287 schema: &SchemaRef,
288 ) -> Result<RecordBatch, SerdeError> {
289 if records.is_empty() {
290 return Ok(RecordBatch::new_empty(schema.clone()));
291 }
292
293 let mut guard = self.decoder.lock();
294 let decoder = if let Some(d) = guard.as_mut() {
295 d
296 } else {
297 let mut builder = ReaderBuilder::new()
298 .with_batch_size(DECODER_BATCH_CAPACITY)
299 .with_writer_schema_store(self.schema_store.clone());
300 if let Some(reader) = &self.reader_schema {
301 builder = builder.with_reader_schema(reader.clone());
302 }
303 if let Some(projection) = &self.projection {
304 builder = builder.with_projection(projection.clone());
305 }
306 let d = builder
307 .build_decoder()
308 .map_err(|e| SerdeError::MalformedInput(format!("failed to build decoder: {e}")))?;
309 guard.insert(d)
310 };
311
312 let mut partials: Vec<RecordBatch> = Vec::new();
313 for record in records {
314 let mut offset = 0;
315 while offset < record.len() {
316 let consumed = decoder
317 .decode(&record[offset..])
318 .map_err(|e| SerdeError::MalformedInput(format!("Avro decode error: {e}")))?;
319 if consumed == 0 {
320 break;
321 }
322 offset += consumed;
323 if decoder.batch_is_full() {
324 if let Some(b) = decoder
325 .flush()
326 .map_err(|e| SerdeError::MalformedInput(format!("Avro flush: {e}")))?
327 {
328 partials.push(b);
329 }
330 }
331 }
332 }
333 if let Some(b) = decoder
334 .flush()
335 .map_err(|e| SerdeError::MalformedInput(format!("Avro flush: {e}")))?
336 {
337 partials.push(b);
338 }
339
340 match partials.len() {
341 0 => Err(SerdeError::MalformedInput("no records decoded".into())),
342 1 => {
343 let batch = partials.remove(0);
344 if batch.schema().as_ref() == schema.as_ref() {
345 return Ok(batch);
346 }
347 RecordBatch::try_new(Arc::clone(schema), batch.columns().to_vec()).map_err(
348 |error| {
349 SerdeError::MalformedInput(format!("committed reader mismatch: {error}"))
350 },
351 )
352 }
353 _ => arrow_select::concat::concat_batches(schema, &partials)
354 .map_err(|e| SerdeError::MalformedInput(format!("concat: {e}"))),
355 }
356 }
357
358 fn format(&self) -> Format {
359 Format::Avro
360 }
361
362 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
363 Some(self)
364 }
365}
366
367impl std::fmt::Debug for AvroDeserializer {
368 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 f.debug_struct("AvroDeserializer")
370 .field(
371 "writer_ids",
372 &self.writers.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
373 )
374 .field("has_registry", &self.schema_registry.is_some())
375 .finish_non_exhaustive()
376 }
377}
378
379#[cfg(test)]
380mod tests;
381
382struct WriterFetchObservation {
383 metrics: Option<super::KafkaSourceMetrics>,
384 started: std::time::Instant,
385 succeeded: bool,
386}
387
388impl WriterFetchObservation {
389 fn new(metrics: Option<super::KafkaSourceMetrics>) -> Self {
390 if let Some(metrics) = &metrics {
391 metrics.schema_cache_misses.inc();
392 metrics.schema_unresolved_records.inc();
393 }
394 Self {
395 metrics,
396 started: std::time::Instant::now(),
397 succeeded: false,
398 }
399 }
400}
401
402impl Drop for WriterFetchObservation {
403 fn drop(&mut self) {
404 if let Some(metrics) = &self.metrics {
405 metrics.schema_unresolved_records.dec();
406 metrics
407 .schema_fetch_microseconds
408 .inc_by(u64::try_from(self.started.elapsed().as_micros()).unwrap_or(u64::MAX));
409 if !self.succeeded {
410 metrics.schema_fetch_failures.inc();
411 }
412 }
413 }
414}