laminar_connectors/rabbitmq/sink/
mod.rs1mod properties;
11
12use std::collections::VecDeque;
13use std::time::Duration;
14
15use arrow_array::RecordBatch;
16use arrow_schema::SchemaRef;
17use async_trait::async_trait;
18use lapin::options::{BasicPublishOptions, ConfirmSelectOptions};
19use lapin::{BasicProperties, Channel, Confirmation, Connection, PublisherConfirm};
20
21use super::config::{DeliveryMode, RabbitMqSinkConfig};
22use super::connection::{close, connect, prepare_exchange};
23use super::metrics::{saturating, SinkMetrics};
24use crate::config::ConnectorConfig;
25use crate::connector::{
26 SinkConnector, SinkConsistency, SinkContract, SinkInputMode, SinkTopology, WriteResult,
27};
28use crate::error::ConnectorError;
29use crate::serde::RecordSerializer;
30use properties::{base_properties, payload_schema, BoundProperties};
31
32pub struct RabbitMqSink {
34 schema: SchemaRef,
35 registry: Option<prometheus::Registry>,
36 config: Option<RabbitMqSinkConfig>,
37 writer: Option<Writer>,
38 runtime: Option<Runtime>,
39 failure: Option<String>,
40}
41
42struct Writer {
43 serializer: Box<dyn RecordSerializer>,
44 payload_indices: Vec<usize>,
45 base: BasicProperties,
46}
47
48struct Runtime {
49 connection: Connection,
50 channel: Channel,
51 pending: VecDeque<(PublisherConfirm, usize)>,
52 pending_bytes: usize,
53 metrics: SinkMetrics,
54}
55
56#[derive(Default)]
58struct Drained {
59 confirmed: usize,
60 nacked: usize,
61 returned: Option<String>,
62 ambiguous: Option<String>,
63}
64
65impl Drained {
66 fn into_result(self) -> Result<usize, ConnectorError> {
67 if let Some(cause) = self.ambiguous {
68 return Err(ConnectorError::outcome_unknown(
69 format!(
70 "RabbitMQ publish outcome unknown after {} confirmed: {cause}",
71 self.confirmed
72 ),
73 true,
74 ));
75 }
76 if let Some(cause) = self.returned {
77 return Err(ConnectorError::ConfigurationError(format!(
78 "a mandatory publish was returned as unroutable ({cause}); bind a queue to the \
79 exchange for this routing key"
80 )));
81 }
82 if self.nacked > 0 {
83 return Err(ConnectorError::WriteError(format!(
84 "the broker negatively confirmed {} publish(es)",
85 self.nacked
86 )));
87 }
88 Ok(self.confirmed)
89 }
90}
91
92impl Runtime {
93 async fn drain(
95 &mut self,
96 keep: usize,
97 keep_bytes: usize,
98 deadline: tokio::time::Instant,
99 ) -> Result<usize, ConnectorError> {
100 let mut drained = Drained::default();
101 while self.pending.len() > keep || self.pending_bytes > keep_bytes {
102 let Some((confirm, bytes)) = self.pending.pop_front() else {
103 break;
104 };
105 self.pending_bytes -= bytes;
106 match tokio::time::timeout_at(deadline, confirm).await {
107 Ok(Ok(Confirmation::Ack(None))) => drained.confirmed += 1,
108 Ok(Ok(Confirmation::Ack(Some(returned)))) => {
109 self.metrics.returned.inc();
110 drained.returned.get_or_insert_with(|| {
111 format!("{} {}", returned.reply_code, returned.reply_text.as_str())
112 });
113 }
114 Ok(Ok(Confirmation::Nack(_))) => {
115 self.metrics.nacked.inc();
116 drained.nacked += 1;
117 }
118 Ok(Ok(Confirmation::NotRequested)) => {
119 drained
120 .ambiguous
121 .get_or_insert_with(|| "the channel is not in confirm mode".to_string());
122 }
123 Ok(Err(error)) => {
124 self.metrics.ambiguous.inc();
125 drained.ambiguous.get_or_insert_with(|| error.to_string());
126 }
127 Err(_) => {
128 self.metrics.ambiguous.inc();
129 drained
130 .ambiguous
131 .get_or_insert_with(|| "no broker confirm before the deadline".to_string());
132 }
133 }
134 }
135 self.metrics.confirmed.inc_by(drained.confirmed as u64);
136 self.metrics.pending.set(saturating(self.pending.len()));
137 self.metrics
138 .pending_bytes
139 .set(saturating(self.pending_bytes));
140 drained.into_result()
141 }
142}
143
144impl RabbitMqSink {
145 #[must_use]
147 pub fn new(schema: SchemaRef, registry: Option<&prometheus::Registry>) -> Self {
148 Self {
149 schema,
150 registry: registry.cloned(),
151 config: None,
152 writer: None,
153 runtime: None,
154 failure: None,
155 }
156 }
157
158 fn fail(&mut self, error: ConnectorError) -> ConnectorError {
159 tracing::error!(%error, "RabbitMQ sink generation ended");
160 self.failure.get_or_insert(error.to_string());
161 error
162 }
163
164 fn live(&mut self) -> Result<(&RabbitMqSinkConfig, &Writer, &mut Runtime), ConnectorError> {
165 if let Some(cause) = &self.failure {
166 return Err(ConnectorError::InvalidState {
167 expected: "a live RabbitMQ sink".into(),
168 actual: cause.clone(),
169 });
170 }
171 match (&self.config, &self.writer, self.runtime.as_mut()) {
172 (Some(config), Some(writer), Some(runtime)) => Ok((config, writer, runtime)),
173 _ => Err(ConnectorError::InvalidState {
174 expected: "an open RabbitMQ sink".into(),
175 actual: "open() has not completed".into(),
176 }),
177 }
178 }
179
180 fn writer(parsed: &RabbitMqSinkConfig, input: &SchemaRef) -> Result<Writer, ConnectorError> {
181 let payload = payload_schema(parsed, input)?;
182 crate::serde::schema_contract::validate_writer(parsed.format, &payload)?;
183 let payload_indices = payload
184 .fields()
185 .iter()
186 .map(|field| input.index_of(field.name()))
187 .collect::<Result<_, _>>()
188 .map_err(|error| ConnectorError::SchemaMismatch(error.to_string()))?;
189 Ok(Writer {
190 serializer: crate::serde::create_serializer(parsed.format)?,
191 base: base_properties(parsed, &payload),
192 payload_indices,
193 })
194 }
195
196 async fn publish(&mut self, batch: &RecordBatch) -> Result<WriteResult, ConnectorError> {
197 let (config, writer, runtime) = self.live()?;
198 let rows = BoundProperties::bind(config, batch)?;
199 let payload = batch
200 .project(&writer.payload_indices)
201 .map_err(|error| ConnectorError::SchemaMismatch(error.to_string()))?;
202 let records = writer.serializer.serialize(&payload)?;
203 if records.len() != batch.num_rows() {
204 return Err(ConnectorError::Internal(format!(
205 "RabbitMQ serializer returned {} records for {} rows",
206 records.len(),
207 batch.num_rows()
208 )));
209 }
210 let deadline = tokio::time::Instant::now() + config.confirm_timeout;
211 let options = BasicPublishOptions {
212 mandatory: config.mandatory,
213 ..BasicPublishOptions::default()
214 };
215 let mut bytes = 0_u64;
216 for (row, record) in records.iter().enumerate() {
217 let keep = config.max_pending - 1;
218 let keep_bytes = config.max_pending_bytes.saturating_sub(record.len());
219 runtime.drain(keep, keep_bytes, deadline).await?;
220 let confirm = runtime
221 .channel
222 .basic_publish(
223 config.exchange.as_str().into(),
224 rows.routing_key(row).into(),
225 options,
226 record,
227 rows.properties(row, &writer.base),
228 )
229 .await
230 .map_err(|error| {
231 let prior = row > 0 || !runtime.pending.is_empty();
232 if prior {
233 ConnectorError::outcome_unknown(format!("RabbitMQ publish: {error}"), true)
234 } else {
235 ConnectorError::WriteError(format!("RabbitMQ publish: {error}"))
236 }
237 })?;
238 runtime.pending.push_back((confirm, record.len()));
239 runtime.pending_bytes += record.len();
240 bytes += record.len() as u64;
241 }
242 runtime.metrics.published.inc_by(records.len() as u64);
243 runtime.metrics.bytes.inc_by(bytes);
244 runtime
245 .metrics
246 .pending
247 .set(saturating(runtime.pending.len()));
248 runtime
249 .metrics
250 .pending_bytes
251 .set(saturating(runtime.pending_bytes));
252 Ok(WriteResult::new(records.len(), bytes))
253 }
254}
255
256#[async_trait]
257impl SinkConnector for RabbitMqSink {
258 fn contract(&self, config: &ConnectorConfig) -> Result<SinkContract, ConnectorError> {
259 let parsed = RabbitMqSinkConfig::from_config(config)?;
260 let consistency = match parsed.delivery_mode {
261 DeliveryMode::Persistent => SinkConsistency::DurableAtLeastOnce,
262 DeliveryMode::Transient => SinkConsistency::Ephemeral,
263 };
264 Ok(SinkContract::new(
265 consistency,
266 SinkTopology::MultiWriter,
267 SinkInputMode::AppendOnly,
268 ))
269 }
270
271 async fn resolve_schema(
272 &mut self,
273 config: &ConnectorConfig,
274 input: SchemaRef,
275 ) -> Result<crate::schema::resolution::SchemaBinding, ConnectorError> {
276 use crate::schema::resolution::{logical_binding, SchemaDirection, SchemaOrigin};
277 let parsed = RabbitMqSinkConfig::from_config(config)?;
278 Self::writer(&parsed, &input)?;
279 logical_binding(config, SchemaDirection::Sink, SchemaOrigin::Query, &input)
280 }
281
282 async fn open(&mut self, config: &ConnectorConfig) -> Result<(), ConnectorError> {
283 let parsed = RabbitMqSinkConfig::from_config(config)?;
284 let input = config
285 .arrow_schema()
286 .or_else(|| {
287 config
288 .schema_binding()
289 .map(|binding| std::sync::Arc::new(binding.logical.clone()))
290 })
291 .unwrap_or_else(|| self.schema.clone());
292 let writer = Self::writer(&parsed, &input)?;
293 let metrics = SinkMetrics::new(&parsed.connection.connection_name, self.registry.as_ref())?;
294 let connection = connect(&parsed.connection).await?;
295 let channel = connection.create_channel().await.map_err(|error| {
296 ConnectorError::ConnectionFailed(format!("RabbitMQ channel: {error}"))
297 })?;
298 channel
299 .confirm_select(ConfirmSelectOptions::default())
300 .await
301 .map_err(|error| {
302 ConnectorError::ConnectionFailed(format!("confirm.select: {error}"))
303 })?;
304 prepare_exchange(&channel, &parsed.exchange, parsed.exchange_declare.as_ref()).await?;
305 tracing::info!(
306 exchange = %parsed.exchange,
307 mandatory = parsed.mandatory,
308 delivery_mode = ?parsed.delivery_mode,
309 endpoints = ?parsed.connection,
310 "RabbitMQ sink publishing"
311 );
312 self.schema = input;
313 self.writer = Some(writer);
314 self.runtime = Some(Runtime {
315 connection,
316 channel,
317 pending: VecDeque::new(),
318 pending_bytes: 0,
319 metrics,
320 });
321 self.config = Some(parsed);
322 self.failure = None;
323 Ok(())
324 }
325
326 async fn write_batch(&mut self, batch: &RecordBatch) -> Result<WriteResult, ConnectorError> {
327 match self.publish(batch).await {
328 Ok(result) => Ok(result),
329 Err(error) if self.failure.is_some() => Err(error),
330 Err(error) => Err(self.fail(error)),
331 }
332 }
333
334 fn schema(&self) -> SchemaRef {
335 self.schema.clone()
336 }
337
338 fn suggested_write_timeout(&self) -> Duration {
339 self.config
341 .as_ref()
342 .map_or(Duration::from_secs(30), |config| config.confirm_timeout)
343 }
344
345 async fn flush(&mut self) -> Result<(), ConnectorError> {
346 let (config, _, runtime) = self.live()?;
347 let deadline = tokio::time::Instant::now() + config.confirm_timeout;
348 match runtime.drain(0, 0, deadline).await {
349 Ok(_) => Ok(()),
350 Err(error) => Err(self.fail(error)),
351 }
352 }
353
354 async fn close(&mut self) -> Result<(), ConnectorError> {
355 let drained = if self.failure.is_none() && self.runtime.is_some() {
356 self.flush().await
357 } else {
358 Ok(())
359 };
360 let closed = match self.runtime.take() {
361 Some(runtime) => {
362 close(
363 &runtime.channel,
364 &runtime.connection,
365 Duration::from_secs(5),
366 )
367 .await
368 }
369 None => Ok(()),
370 };
371 drained.and(closed)
372 }
373}
374
375#[cfg(test)]
376mod tests;