Skip to main content

laminar_connectors/kafka/avro_serializer/
mod.rs

1//! Avro serialization using `arrow-avro` with Confluent Schema Registry.
2//!
3//! `AvroSerializer` implements `RecordSerializer` by wrapping the
4//! `arrow-avro` `Writer` with SOE format, producing per-record payloads
5//! with the Confluent wire format prefix (`0x00` + 4-byte BE schema ID
6//! + Avro body).
7
8use std::sync::atomic::{AtomicU32, Ordering};
9use std::sync::Arc;
10
11use arrow_array::RecordBatch;
12use arrow_avro::schema::FingerprintStrategy;
13use arrow_avro::writer::format::AvroSoeFormat;
14use arrow_avro::writer::{Encoder, WriterBuilder};
15use arrow_schema::SchemaRef;
16use parking_lot::Mutex;
17
18use crate::error::SerdeError;
19use crate::kafka::schema_registry::SchemaRegistryClient;
20use crate::serde::{Format, RecordSerializer};
21
22/// Avro serializer backed by `arrow-avro` with optional Schema Registry.
23///
24/// Produces per-row byte payloads in the Confluent wire format suitable
25/// for individual Kafka producer messages.
26pub struct AvroSerializer {
27    /// Schema ID shared with `KafkaSink` for post-registration updates.
28    schema_id: Arc<AtomicU32>,
29    /// Arrow schema for the records being serialized.
30    schema: SchemaRef,
31    /// Optional Schema Registry client for schema registration.
32    schema_registry: Option<Arc<SchemaRegistryClient>>,
33    encoder: Mutex<Option<(u32, Encoder)>>,
34}
35
36impl AvroSerializer {
37    /// Creates a new Avro serializer with a known schema ID.
38    ///
39    /// Each serialized record is prefixed with `0x00` + `schema_id` (4-byte BE).
40    #[must_use]
41    pub fn new(schema: SchemaRef, schema_id: u32) -> Self {
42        Self {
43            schema_id: Arc::new(AtomicU32::new(schema_id)),
44            schema,
45            schema_registry: None,
46            encoder: Mutex::new(None),
47        }
48    }
49
50    /// Creates a new Avro serializer with a shared schema ID handle.
51    ///
52    /// The `KafkaSink` retains a clone of the `Arc<AtomicU32>` so it can
53    /// update the schema ID after registration without downcasting.
54    #[must_use]
55    pub fn with_shared_schema_id(
56        schema: SchemaRef,
57        schema_id: Arc<AtomicU32>,
58        registry: Option<Arc<SchemaRegistryClient>>,
59    ) -> Self {
60        Self {
61            schema_id,
62            schema,
63            schema_registry: registry,
64            encoder: Mutex::new(None),
65        }
66    }
67
68    /// Creates a new Avro serializer with Schema Registry integration.
69    #[must_use]
70    pub fn with_schema_registry(
71        schema: SchemaRef,
72        schema_id: u32,
73        registry: Arc<SchemaRegistryClient>,
74    ) -> Self {
75        Self {
76            schema_id: Arc::new(AtomicU32::new(schema_id)),
77            schema,
78            schema_registry: Some(registry),
79            encoder: Mutex::new(None),
80        }
81    }
82
83    /// Returns a shared handle to the schema ID for external updates.
84    #[must_use]
85    pub fn schema_id_handle(&self) -> Arc<AtomicU32> {
86        Arc::clone(&self.schema_id)
87    }
88
89    /// Returns the current schema ID.
90    #[must_use]
91    pub fn schema_id(&self) -> u32 {
92        self.schema_id.load(Ordering::Relaxed)
93    }
94
95    /// Returns whether a Schema Registry client is configured.
96    #[must_use]
97    pub fn has_schema_registry(&self) -> bool {
98        self.schema_registry.is_some()
99    }
100
101    /// Prepare the native writer once, before activation.
102    ///
103    /// # Errors
104    /// Rejects invalid wire IDs and native schemas unsupported by the installed codec.
105    pub fn prepare(&self) -> Result<(), SerdeError> {
106        let id = self.schema_id();
107        let mut prepared = self.encoder.lock();
108        if prepared
109            .as_ref()
110            .is_some_and(|(existing, _)| *existing == id)
111        {
112            return Ok(());
113        }
114        if id == 0 || id > i32::MAX as u32 {
115            return Err(SerdeError::MalformedInput(
116                "Avro writer requires a positive signed schema ID".into(),
117            ));
118        }
119        let encoder = WriterBuilder::new(self.schema.as_ref().clone())
120            .with_fingerprint_strategy(FingerprintStrategy::Id(id))
121            .build_encoder::<AvroSoeFormat>()
122            .map_err(|error| {
123                SerdeError::MalformedInput(format!("unsupported Avro writer: {error}"))
124            })?;
125        *prepared = Some((id, encoder));
126        Ok(())
127    }
128
129    fn encode(&self, batch: &RecordBatch) -> Result<arrow_avro::writer::EncodedRows, SerdeError> {
130        self.prepare()?;
131        let mut prepared = self.encoder.lock();
132        let (_, encoder) = prepared
133            .as_mut()
134            .ok_or_else(|| SerdeError::MalformedInput("Avro writer was not prepared".into()))?;
135        let result = encoder.encode(batch);
136        // Drain partial output on failure so a later call cannot publish bytes from a failed batch.
137        let rows = encoder.flush();
138        result
139            .map_err(|error| SerdeError::MalformedInput(format!("Avro encode error: {error}")))?;
140        Ok(rows)
141    }
142}
143
144impl RecordSerializer for AvroSerializer {
145    fn serialize(&self, batch: &RecordBatch) -> Result<Vec<Vec<u8>>, SerdeError> {
146        if batch.num_rows() == 0 {
147            return Ok(Vec::new());
148        }
149        Ok(self.encode(batch)?.iter().map(|row| row.to_vec()).collect())
150    }
151
152    fn serialize_batch(&self, batch: &RecordBatch) -> Result<Vec<u8>, SerdeError> {
153        if batch.num_rows() == 0 {
154            return Ok(Vec::new());
155        }
156        let rows = self.encode(batch)?;
157        let mut output = Vec::new();
158        for row in rows.iter() {
159            output.extend_from_slice(&row);
160        }
161        Ok(output)
162    }
163
164    fn format(&self) -> Format {
165        Format::Avro
166    }
167}
168
169impl std::fmt::Debug for AvroSerializer {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        f.debug_struct("AvroSerializer")
172            .field("schema_id", &self.schema_id.load(Ordering::Relaxed))
173            .field("has_registry", &self.schema_registry.is_some())
174            .finish_non_exhaustive()
175    }
176}
177
178#[cfg(test)]
179mod tests;