GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [connectors] Retain Kafka output through broker outages · feldera/feldera@6d6aa60

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • crates
    • connectors/src/transport
      • kafka.rs
      • kafka
        • ft
          • output.rs
        • nonft.rs
        • nonft
          • output.rs
          • test.rs
    • feldera-types/src/transport
      • kafka.rs
  • docs.feldera.com/docs
    • changelog.md
    • connectors/sinks
      • kafka.md
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎crates/connectors/src/transport/kafka.rs‎

Copy file name to clipboardExpand all lines: crates/connectors/src/transport/kafka.rs
+66-9Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -257,39 +257,75 @@ pub fn build_headers(headers: &Vec<KafkaHeader>) -> OwnedHeaders {
257257
result
258258
}
259259

260+
/// A produce call that librdkafka refused for a reason other than a full
261+
/// queue. The exactly-once sink reads the Kafka error to decide whether the
262+
/// open transaction must be aborted.
263+
#[derive(Debug)]
264+
pub(crate) struct SendError {
265+
pub topic: String,
266+
pub error: KafkaError,
267+
}
268+
269+
impl std::fmt::Display for SendError {
270+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271+
write!(
272+
f,
273+
"error sending Kafka message to topic '{}': {}",
274+
self.topic, self.error
275+
)
276+
}
277+
}
278+
279+
impl std::error::Error for SendError {
280+
// `Display` prints the Kafka error already. Returning it here too would
281+
// make `{:#}` print it twice.
282+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
283+
None
284+
}
285+
}
286+
260287
/// Hands `record` to `producer`, waiting out a full producer queue.
261288
///
262289
/// The wait is unbounded, so it ends on `shutdown`: this runs on the
263290
/// controller's output thread, a DBSP aux thread that `RuntimeHandle::kill`
264291
/// joins, and a queue that a down broker keeps full would otherwise hold the
265292
/// teardown open for as long as the broker stays down.
293+
///
294+
/// Returns the time spent waiting, zero when the queue had room. `on_wait`
295+
/// receives the cumulative wait after each attempt, including the final
296+
/// attempt on success or failure, so metrics include interrupted waits.
266297
pub fn kafka_send<T1, T2, C>(
267298
producer: &ThreadedProducer<C>,
268299
topic: &str,
269300
mut record: BaseRecord<T1, T2, C::DeliveryOpaque>,
270301
shutdown: &CancellationToken,
271-
) -> AnyResult<()>
302+
on_wait: &mut dyn FnMut(Duration),
303+
) -> AnyResult<Duration>
272304
where
273305
T1: ToBytes + ?Sized,
274306
T2: ToBytes + ?Sized,
275307
C: ProducerContext,
276308
{
277309
let mut polling_interval = Duration::from_micros(10);
278-
let mut start;
310+
let mut start: Option<Instant> = None;
279311

280312
loop {
281-
match producer.send(record) {
282-
Ok(()) => return Ok(()),
313+
let result = producer.send(record);
314+
if let Some(start) = start {
315+
on_wait(start.elapsed());
316+
}
317+
match result {
318+
Ok(()) => return Ok(start.map(|start| start.elapsed()).unwrap_or_default()),
283319
Err((e, r)) => match e {
284320
KafkaError::MessageProduction(e) if is_retriable_send_error(e) => {
285321
// Start timing after the first error.
286-
start = Some(Instant::now());
322+
let start = *start.get_or_insert_with(Instant::now);
287323
record = r;
288324

289325
if shutdown.is_cancelled() {
290326
bail!(
291-
"gave up sending a message to Kafka topic '{topic}' after {:?} because the pipeline is shutting down (error: {e})",
292-
start.unwrap().elapsed()
327+
"gave up sending a message to Kafka topic '{topic}' after {:?} because the pipeline is shutting down (error: {e})",
328+
start.elapsed()
293329
);
294330
}
295331

@@ -298,7 +334,7 @@ where
298334
warn!(
299335
"Attempts to send a message to Kafka topic '{}' have failed for {:?}, will keep retrying (error: {e})",
300336
&topic,
301-
start.unwrap().elapsed()
337+
start.elapsed()
302338
);
303339
}
304340

@@ -307,7 +343,13 @@ where
307343
polling_interval = min(polling_interval * 2, MAX_POLLING_INTERVAL);
308344
}
309345
}
310-
_ => bail!("error sending Kafka message: {e}"),
346+
_ => {
347+
return Err(SendError {
348+
topic: topic.to_owned(),
349+
error: e,
350+
}
351+
.into());
352+
}
311353
},
312354
}
313355
}
@@ -607,6 +649,7 @@ mod send_tests {
607649
use super::{MAX_POLLING_INTERVAL, kafka_send};
608650
use rdkafka::ClientConfig;
609651
use rdkafka::producer::{BaseRecord, DefaultProducerContext, ThreadedProducer};
652+
use std::sync::atomic::{AtomicU64, Ordering};
610653
use std::sync::{Arc, mpsc};
611654
use std::time::Duration;
612655
use tokio_util::sync::CancellationToken;
@@ -635,6 +678,7 @@ mod send_tests {
635678
"test_topic",
636679
BaseRecord::<(), [u8], ()>::to("test_topic").payload(b"first".as_slice()),
637680
&shutdown,
681+
&mut |_| {},
638682
)
639683
.expect("the first message must fit in the queue");
640684

@@ -643,12 +687,17 @@ mod send_tests {
643687
let sending_producer = producer.clone();
644688
let sending_shutdown = shutdown.clone();
645689
let (done, send_returned) = mpsc::channel();
690+
let reported_wait = Arc::new(AtomicU64::new(0));
691+
let sending_wait = reported_wait.clone();
646692
let sender = std::thread::spawn(move || {
647693
let result = kafka_send(
648694
&sending_producer,
649695
"test_topic",
650696
BaseRecord::<(), [u8], ()>::to("test_topic").payload(b"second".as_slice()),
651697
&sending_shutdown,
698+
&mut |waited| {
699+
sending_wait.store(waited.as_micros() as u64, Ordering::Release);
700+
},
652701
);
653702
let _ = done.send(result.err());
654703
});
@@ -657,13 +706,21 @@ mod send_tests {
657706
send_returned.recv_timeout(MAX_POLLING_INTERVAL).is_err(),
658707
"a full queue must make `kafka_send` wait"
659708
);
709+
assert!(
710+
reported_wait.load(Ordering::Acquire) > 0,
711+
"report an active wait"
712+
);
660713

661714
shutdown.cancel();
662715
let error = send_returned
663716
.recv_timeout(Duration::from_secs(30))
664717
.expect("a shutdown must end the wait")
665718
.expect("a send that gave up must report an error");
666719
sender.join().unwrap();
720+
assert!(
721+
reported_wait.load(Ordering::Acquire) >= MAX_POLLING_INTERVAL.as_micros() as u64,
722+
"report the entire wait even when shutdown interrupts it"
723+
);
667724

668725
assert!(
669726
error.to_string().contains("shutting down"),
Collapse file

‎crates/connectors/src/transport/kafka/ft/output.rs‎

Copy file name to clipboardExpand all lines: crates/connectors/src/transport/kafka/ft/output.rs
+7-1Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,13 @@ impl OutputEndpoint for KafkaOutputEndpoint {
287287
if let Some(val) = val {
288288
record = record.payload(val);
289289
}
290-
kafka_send(&self.kafka_producer, &self.topic, record, &self.shutdown)?;
290+
kafka_send(
291+
&self.kafka_producer,
292+
&self.topic,
293+
record,
294+
&self.shutdown,
295+
&mut |_| {},
296+
)?;
291297

292298
self.next_partition += 1;
293299
if self.next_partition >= self.n_partitions {
Collapse file

‎crates/connectors/src/transport/kafka/nonft.rs‎

Copy file name to clipboard
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
mod output;
2+
#[cfg(test)]
3+
mod test;
24

35
pub use output::KafkaOutputEndpoint;

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.