1use std::collections::VecDeque;
7use std::future::IntoFuture;
8use std::str::FromStr;
9use std::time::Duration;
10
11use arrow_array::{cast::AsArray, Array, RecordBatch, StringArray};
12use arrow_schema::SchemaRef;
13use async_nats::jetstream::{self, context::PublishAckFuture};
14use async_nats::{Client, HeaderMap, HeaderName, HeaderValue, Subject};
15use async_trait::async_trait;
16use futures_util::stream::{FuturesUnordered, StreamExt};
17
18use super::config::{build_connect_options, Mode, NatsSinkConfig, SubjectSpec};
19use super::metrics::NatsSinkMetrics;
20use super::setup::{classify_connect_error, classify_get_stream_error, track_connection_tasks};
21use crate::config::ConnectorConfig;
22use crate::connector::{
23 ConnectorTaskOwner, ConnectorTaskTracker, SinkConnector, SinkConsistency, SinkContract,
24 SinkInputMode, SinkTopology, WriteResult,
25};
26use crate::error::ConnectorError;
27use crate::serde::{self, RecordSerializer};
28
29mod message;
30
31#[cfg(test)]
32use message::encoded_header_len;
33use message::{
34 build_headers, operation_has_prior_output, resolve_utf8, validate_headers_and_encoded_len,
35 validate_message_size, validate_non_null, validate_publish_subjects,
36};
37
38const MIN_DURABLE_REPLICAS: usize = 3;
41
42fn validate_durable_stream(
43 stream_name: &str,
44 config: &jetstream::stream::Config,
45) -> Result<(), ConnectorError> {
46 if config.storage != jetstream::stream::StorageType::File {
47 return Err(err(&format!(
48 "[LDB-5072] JetStream '{stream_name}' uses {:?} storage; durable at-least-once \
49 requires file-backed storage",
50 config.storage
51 )));
52 }
53 if config.num_replicas < MIN_DURABLE_REPLICAS {
54 return Err(err(&format!(
55 "[LDB-5073] JetStream '{stream_name}' has {} replica(s); production durable \
56 at-least-once requires at least {MIN_DURABLE_REPLICAS}",
57 config.num_replicas
58 )));
59 }
60 if config.no_ack {
61 return Err(err(&format!(
62 "[LDB-5074] JetStream '{stream_name}' disables publish acknowledgements; durable \
63 at-least-once requires broker acknowledgements"
64 )));
65 }
66 Ok(())
67}
68
69pub struct NatsSink {
71 schema: SchemaRef,
72 config: Option<NatsSinkConfig>,
73 serializer: Option<Box<dyn RecordSerializer>>,
74 runtime: Option<Runtime>,
75 metrics: NatsSinkMetrics,
76 pending_acks: VecDeque<PublishAckFuture>,
79 core_dirty: bool,
82 task_owner: ConnectorTaskOwner,
83 task_tracker: ConnectorTaskTracker,
84}
85
86enum Runtime {
87 Core { client: Client },
88 JetStream { context: jetstream::Context },
89}
90
91async fn bounded_nats_setup_until<T, E>(
92 deadline: tokio::time::Instant,
93 total_timeout: Duration,
94 future: impl std::future::Future<Output = Result<T, E>>,
95 classify: impl FnOnce(E) -> ConnectorError,
96) -> Result<T, ConnectorError> {
97 match tokio::time::timeout_at(deadline, future).await {
98 Ok(Ok(value)) => Ok(value),
99 Ok(Err(error)) => Err(classify(error)),
100 Err(_) => Err(ConnectorError::Timeout(timeout_millis(total_timeout))),
101 }
102}
103
104#[derive(Debug)]
105enum PublishEnqueueFailure<E> {
106 Client(E),
107 TimedOut(Duration),
108}
109
110async fn bounded_publish_enqueue<T, E>(
111 deadline: tokio::time::Instant,
112 total_timeout: Duration,
113 future: impl std::future::Future<Output = Result<T, E>>,
114) -> Result<T, PublishEnqueueFailure<E>> {
115 match tokio::time::timeout_at(deadline, future).await {
116 Ok(Ok(value)) => Ok(value),
117 Ok(Err(error)) => Err(PublishEnqueueFailure::Client(error)),
118 Err(_) => Err(PublishEnqueueFailure::TimedOut(total_timeout)),
119 }
120}
121
122fn known_not_dispatched_error(
123 operation: &str,
124 detail: impl std::fmt::Display,
125 retryable: bool,
126 prior_output: bool,
127) -> ConnectorError {
128 if prior_output {
129 ConnectorError::outcome_unknown(
130 format!(
131 "{operation} did not dispatch the current message, but prior NATS output may \
132 already have been applied: {detail}"
133 ),
134 retryable,
135 )
136 } else if retryable {
137 ConnectorError::WriteError(format!(
138 "{operation} did not dispatch the message: {detail}"
139 ))
140 } else {
141 ConnectorError::ConfigurationError(format!(
142 "{operation} rejected the message before dispatch: {detail}"
143 ))
144 }
145}
146
147fn preserve_prior_applied(error: ConnectorError, applied: usize) -> ConnectorError {
148 if applied == 0 || error.is_outcome_unknown() {
149 error
150 } else {
151 let retryable = error.is_transient();
152 ConnectorError::outcome_unknown(
153 format!(
154 "JetStream acknowledgement failed after {applied} earlier publish(es) were \
155 acknowledged: {error}"
156 ),
157 retryable,
158 )
159 }
160}
161
162fn classify_core_publish_failure(
163 failure: PublishEnqueueFailure<async_nats::PublishError>,
164 prior_output: bool,
165) -> ConnectorError {
166 let (detail, retryable) = match failure {
167 PublishEnqueueFailure::Client(error) => {
168 let retryable = matches!(error.kind(), async_nats::client::PublishErrorKind::Send);
169 (error.to_string(), retryable)
170 }
171 PublishEnqueueFailure::TimedOut(timeout) => (
172 format!(
173 "client enqueue timed out after {}ms",
174 timeout_millis(timeout)
175 ),
176 true,
177 ),
178 };
179 known_not_dispatched_error("NATS core publish", detail, retryable, prior_output)
180}
181
182fn classify_jetstream_enqueue_failure(
183 failure: PublishEnqueueFailure<jetstream::context::PublishError>,
184 prior_output: bool,
185) -> ConnectorError {
186 let (detail, retryable) = match failure {
187 PublishEnqueueFailure::Client(error) => {
188 use jetstream::context::PublishErrorKind;
189
190 let invalid_subject = std::error::Error::source(&error)
191 .is_some_and(<dyn std::error::Error>::is::<async_nats::SubjectError>);
192 let retryable = match error.kind() {
193 PublishErrorKind::StreamNotFound
194 | PublishErrorKind::WrongLastMessageId
195 | PublishErrorKind::WrongLastSequence => false,
196 PublishErrorKind::Other if invalid_subject => false,
197 PublishErrorKind::TimedOut
198 | PublishErrorKind::BrokenPipe
199 | PublishErrorKind::MaxAckPending
200 | PublishErrorKind::Other => true,
201 };
202 (error.to_string(), retryable)
203 }
204 PublishEnqueueFailure::TimedOut(timeout) => (
205 format!(
206 "client enqueue timed out after {}ms",
207 timeout_millis(timeout)
208 ),
209 true,
210 ),
211 };
212 known_not_dispatched_error("NATS JetStream publish", detail, retryable, prior_output)
213}
214
215fn timeout_millis(timeout: Duration) -> u64 {
216 u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX)
217}
218
219async fn flush_core(
220 client: &Client,
221 timeout: Duration,
222 operation: &'static str,
223) -> Result<(), ConnectorError> {
224 match tokio::time::timeout(timeout, client.flush()).await {
225 Ok(Ok(())) => Ok(()),
226 Ok(Err(error)) => Err(ConnectorError::outcome_unknown(
227 format!(
228 "{operation} failed after Core publishes were enqueued; whether their bytes \
229 reached the transport is unknown: {error}"
230 ),
231 true,
232 )),
233 Err(_) => Err(ConnectorError::outcome_unknown(
234 format!(
235 "{operation} timed out after {}ms with Core publishes still buffered or in flight",
236 timeout_millis(timeout)
237 ),
238 true,
239 )),
240 }
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244enum AckCertainty {
245 Rejected,
246 OutcomeUnknown,
247}
248
249#[derive(Debug)]
250struct AckFailure {
251 certainty: AckCertainty,
252 detail: String,
253 retryable: bool,
254}
255
256fn classify_jetstream_ack_failure(error: &jetstream::context::PublishError) -> AckFailure {
257 use jetstream::context::PublishErrorKind;
258
259 let source = std::error::Error::source(error);
260 let (certainty, retryable) = match error.kind() {
261 PublishErrorKind::StreamNotFound
262 | PublishErrorKind::WrongLastMessageId
263 | PublishErrorKind::WrongLastSequence => (AckCertainty::Rejected, false),
264 PublishErrorKind::TimedOut | PublishErrorKind::BrokenPipe => {
265 (AckCertainty::OutcomeUnknown, true)
266 }
267 PublishErrorKind::Other => {
268 if let Some(broker_error) =
269 source.and_then(|source| source.downcast_ref::<jetstream::Error>())
270 {
271 let retryable = matches!(broker_error.code(), 408 | 429 | 500..=599);
272 if retryable {
273 (AckCertainty::OutcomeUnknown, true)
274 } else {
275 (AckCertainty::Rejected, false)
276 }
277 } else if source.is_some_and(<dyn std::error::Error>::is::<serde_json::Error>) {
278 (AckCertainty::OutcomeUnknown, false)
280 } else {
281 (AckCertainty::OutcomeUnknown, false)
285 }
286 }
287 PublishErrorKind::MaxAckPending => (AckCertainty::OutcomeUnknown, false),
290 };
291 AckFailure {
292 certainty,
293 detail: error.to_string(),
294 retryable,
295 }
296}
297
298struct AckAggregate {
299 total: usize,
300 applied: usize,
301 rejected: usize,
302 ambiguous: usize,
303 first_error: Option<String>,
304 retryable: bool,
305}
306
307impl Default for AckAggregate {
308 fn default() -> Self {
309 Self {
310 total: 0,
311 applied: 0,
312 rejected: 0,
313 ambiguous: 0,
314 first_error: None,
315 retryable: true,
316 }
317 }
318}
319
320impl AckAggregate {
321 fn record_applied(&mut self) {
322 self.total += 1;
323 self.applied += 1;
324 }
325
326 fn record_failure(&mut self, failure: AckFailure) {
327 self.total += 1;
328 self.retryable &= failure.retryable;
329 match failure.certainty {
330 AckCertainty::Rejected => self.rejected += 1,
331 AckCertainty::OutcomeUnknown => self.ambiguous += 1,
332 }
333 self.first_error.get_or_insert(failure.detail);
334 }
335
336 fn record_unresolved_timeout(&mut self, count: usize, timeout: Duration) {
337 self.total += count;
338 self.ambiguous += count;
339 self.first_error.get_or_insert_with(|| {
340 format!(
341 "{count} acknowledgement(s) remained in flight after {}ms",
342 timeout_millis(timeout)
343 )
344 });
345 }
346
347 fn into_result(self) -> Result<usize, ConnectorError> {
348 if self.rejected == 0 && self.ambiguous == 0 {
349 return Ok(self.applied);
350 }
351 let detail = format!(
352 "{} rejected, {} outcome unknown, {} acknowledged out of {}; first error: {}",
353 self.rejected,
354 self.ambiguous,
355 self.applied,
356 self.total,
357 self.first_error.unwrap_or_else(|| "unknown".into())
358 );
359 if self.ambiguous > 0 || self.applied > 0 {
360 Err(ConnectorError::outcome_unknown(
361 format!("JetStream acknowledgement drain was not atomic: {detail}"),
362 self.retryable,
363 ))
364 } else if self.retryable {
365 Err(ConnectorError::WriteError(format!(
366 "JetStream rejected every publish acknowledgement: {detail}"
367 )))
368 } else {
369 Err(ConnectorError::ConfigurationError(format!(
370 "JetStream rejected every publish acknowledgement: {detail}"
371 )))
372 }
373 }
374}
375
376impl NatsSink {
377 #[must_use]
379 pub fn new(schema: SchemaRef, registry: Option<&prometheus::Registry>) -> Self {
380 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
381 Self {
382 schema,
383 config: None,
384 serializer: None,
385 runtime: None,
386 metrics: NatsSinkMetrics::new(registry),
387 pending_acks: VecDeque::new(),
388 core_dirty: false,
389 task_owner,
390 task_tracker,
391 }
392 }
393
394 #[must_use]
396 pub fn config(&self) -> Option<&NatsSinkConfig> {
397 self.config.as_ref()
398 }
399
400 #[must_use]
402 pub fn metrics_handle(&self) -> &NatsSinkMetrics {
403 &self.metrics
404 }
405}
406
407impl Drop for NatsSink {
408 fn drop(&mut self) {
409 retire_pending_acks(&mut self.pending_acks, &self.metrics, &self.task_owner);
410 }
411}
412
413#[async_trait]
415impl SinkConnector for NatsSink {
416 fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
417 Some(self.task_tracker.clone())
418 }
419
420 fn contract(&self, config: &ConnectorConfig) -> Result<SinkContract, ConnectorError> {
421 let cfg = NatsSinkConfig::from_config(config)?;
422 let consistency = match cfg.mode {
423 Mode::Core => SinkConsistency::Ephemeral,
424 Mode::JetStream => SinkConsistency::DurableAtLeastOnce,
425 };
426 Ok(SinkContract::new(
427 consistency,
428 SinkTopology::MultiWriter,
429 SinkInputMode::AppendOnly,
430 ))
431 }
432
433 async fn open(&mut self, config: &ConnectorConfig) -> Result<(), ConnectorError> {
434 if self.core_dirty {
435 return Err(ConnectorError::InvalidState {
436 expected: "a new sink generation after an unresolved Core flush".into(),
437 actual: "this sink still has unconfirmed Core publishes".into(),
438 });
439 }
440 let cfg = NatsSinkConfig::from_config(config)?;
441 self.serializer = Some(
442 serde::create_serializer(cfg.format)
443 .map_err(|e| err(&format!("serializer for format {:?}: {e}", cfg.format)))?,
444 );
445 let setup_timeout = cfg.ack_timeout;
446 let setup_deadline = tokio::time::Instant::now() + setup_timeout;
447 let connect_options = track_connection_tasks(
448 build_connect_options(&cfg.auth, &cfg.tls)?,
449 &self.task_owner,
450 "sink",
451 )?;
452 let client = bounded_nats_setup_until(
453 setup_deadline,
454 setup_timeout,
455 Box::pin(connect_options.connect(&cfg.servers)),
456 |error| classify_connect_error(&error),
457 )
458 .await?;
459 self.runtime = Some(match cfg.mode {
460 Mode::Core => Runtime::Core { client },
461 Mode::JetStream => {
462 let context = jetstream::context::ContextBuilder::new()
466 .timeout(cfg.ack_timeout)
467 .ack_timeout(cfg.ack_timeout)
468 .max_ack_inflight(cfg.max_pending)
469 .backpressure_on_inflight(false)
470 .build(client);
471 let stream_name = cfg
472 .stream
473 .as_deref()
474 .expect("NatsSinkConfig validates the JetStream target");
475 let stream = bounded_nats_setup_until(
476 setup_deadline,
477 setup_timeout,
478 context.get_stream(stream_name),
479 |error| classify_get_stream_error(&error, stream_name),
480 )
481 .await?;
482 let info = stream.cached_info();
483 validate_durable_stream(stream_name, &info.config)?;
484 if cfg.dedup_id_column.is_some() {
485 let actual = info.config.duplicate_window;
486 if actual < cfg.min_duplicate_window {
487 return Err(err(&format!(
488 "[LDB-5056] stream '{stream_name}' has duplicate_window={actual:?}, \
489 below the configured minimum {:?}. Replay or redelivery could land \
490 outside the dedup horizon. Reconfigure the stream or lower \
491 'min.duplicate.window.ms'.",
492 cfg.min_duplicate_window,
493 )));
494 }
495 }
496 Runtime::JetStream { context }
497 }
498 });
499 self.config = Some(cfg);
500 Ok(())
501 }
502
503 async fn write_batch(&mut self, batch: &RecordBatch) -> Result<WriteResult, ConnectorError> {
504 let Self {
506 config,
507 serializer,
508 runtime,
509 pending_acks,
510 core_dirty,
511 metrics,
512 task_owner,
513 task_tracker: _,
514 schema: _,
515 } = self;
516 let cfg = config.as_ref().ok_or_else(|| err("sink: open() first"))?;
517 let write_timeout = cfg.ack_timeout;
520 let write_deadline = tokio::time::Instant::now() + write_timeout;
521 let ser = serializer
522 .as_ref()
523 .ok_or_else(|| err("sink: open() first"))?;
524 let rt = runtime.as_mut().ok_or_else(|| err("sink: open() first"))?;
525
526 let subject_col = match &cfg.subject {
527 SubjectSpec::Column(name) => Some(resolve_utf8(batch, name)?),
528 SubjectSpec::Literal(_) => None,
529 };
530 let header_cols: Vec<(&HeaderName, &StringArray)> = cfg
531 .header_columns
532 .iter()
533 .map(|name| resolve_utf8(batch, name.as_ref()).map(|array| (name, array)))
534 .collect::<Result<_, _>>()?;
535 let expected_stream = cfg.stream.as_deref();
538 let dedup_col = cfg
542 .dedup_id_column
543 .as_deref()
544 .map(|n| resolve_utf8(batch, n).map(|arr| (n, arr)))
545 .transpose()?;
546
547 if let Some(arr) = subject_col {
550 let SubjectSpec::Column(name) = &cfg.subject else {
551 unreachable!("subject column exists only for a column subject")
552 };
553 validate_non_null(arr, "subject.column", name)?;
554 }
555 validate_publish_subjects(&cfg.subject, subject_col, batch.num_rows())?;
556 if let Some((name, arr)) = dedup_col {
557 validate_non_null(arr, "dedup.id.column", name)?;
558 }
559
560 let records = ser
561 .serialize(batch)
562 .map_err(|e| err(&format!("serialize batch: {e}")))?;
563 if records.len() != batch.num_rows() {
564 return Err(ConnectorError::Internal(format!(
565 "NATS serializer returned {} records for a {}-row batch",
566 records.len(),
567 batch.num_rows()
568 )));
569 }
570
571 let max_payload = match &*rt {
575 Runtime::Core { client } => client.server_info().max_payload,
576 Runtime::JetStream { context } => context.client().server_info().max_payload,
577 };
578 for (row, payload) in records.iter().enumerate() {
579 let msg_id = dedup_col.map(|(_, array)| array.value(row));
580 let header_len =
581 validate_headers_and_encoded_len(expected_stream, msg_id, &header_cols, row)?;
582 validate_message_size(row, payload.len(), header_len, max_payload)?;
583 }
584
585 let mut bytes_total: u64 = 0;
586 let mut rows_written: usize = 0;
587 let mut acknowledged_in_write: usize = 0;
588 for (row, payload) in records.into_iter().enumerate() {
589 let subject = match (&cfg.subject, subject_col) {
590 (SubjectSpec::Literal(subject), _) => subject.as_str(),
591 (SubjectSpec::Column(_), Some(array)) => array.value(row),
592 (SubjectSpec::Column(_), None) => unreachable!("resolved above"),
593 };
594 let subject = Subject::from(subject);
595 let msg_id = dedup_col.map(|(_, array)| array.value(row));
596 let headers = build_headers(expected_stream, msg_id, &header_cols, row)?;
597 let payload = bytes::Bytes::from(payload);
598 let payload_len = payload.len() as u64;
599
600 match rt {
601 Runtime::Core { client } => {
602 let prior_output = *core_dirty;
603 let result = if let Some(h) = headers {
604 bounded_publish_enqueue(
605 write_deadline,
606 write_timeout,
607 client.publish_with_headers(subject, h, payload),
608 )
609 .await
610 } else {
611 bounded_publish_enqueue(
612 write_deadline,
613 write_timeout,
614 client.publish(subject, payload),
615 )
616 .await
617 };
618 match result {
619 Ok(()) => *core_dirty = true,
620 Err(failure) => {
621 metrics.record_publish_error();
622 return Err(classify_core_publish_failure(failure, prior_output));
623 }
624 }
625 }
626 Runtime::JetStream { context } => {
627 if pending_acks.len() >= cfg.max_pending {
628 let acknowledged = drain_acks_until(
629 pending_acks,
630 metrics,
631 task_owner,
632 write_deadline,
633 write_timeout,
634 )
635 .await
636 .map_err(|error| preserve_prior_applied(error, acknowledged_in_write))?;
637 acknowledged_in_write += acknowledged;
638 }
639 let prior_output = operation_has_prior_output(
640 rows_written,
641 pending_acks.len(),
642 acknowledged_in_write,
643 );
644 let publish_result = if let Some(h) = headers {
645 bounded_publish_enqueue(
646 write_deadline,
647 write_timeout,
648 context.publish_with_headers(subject, h, payload),
649 )
650 .await
651 } else {
652 bounded_publish_enqueue(
653 write_deadline,
654 write_timeout,
655 context.publish(subject, payload),
656 )
657 .await
658 };
659 match publish_result {
660 Ok(fut) => pending_acks.push_back(fut),
661 Err(failure) => {
662 metrics.record_publish_error();
663 metrics.set_pending_futures(pending_acks.len());
664 return Err(classify_jetstream_enqueue_failure(failure, prior_output));
665 }
666 }
667 }
668 }
669
670 metrics.record_published_row(payload_len);
672 rows_written += 1;
673 bytes_total += payload_len;
674 }
675
676 metrics.set_pending_futures(pending_acks.len());
677 Ok(WriteResult::new(rows_written, bytes_total))
678 }
679
680 fn schema(&self) -> SchemaRef {
681 self.schema.clone()
682 }
683
684 fn suggested_write_timeout(&self) -> Duration {
685 self.config
688 .as_ref()
689 .map_or(Duration::from_secs(30), |config| config.ack_timeout)
690 }
691
692 async fn flush(&mut self) -> Result<(), ConnectorError> {
693 let timeout = self
694 .config
695 .as_ref()
696 .map_or(Duration::from_secs(30), |c| c.ack_timeout);
697 if let Some(Runtime::Core { client }) = self.runtime.as_ref() {
698 if !self.core_dirty {
699 return Ok(());
700 }
701 let result = flush_core(client, timeout, "NATS core flush").await;
702 if result.is_ok() {
703 self.core_dirty = false;
704 }
705 return result;
706 }
707 drain_acks(
708 &mut self.pending_acks,
709 &self.metrics,
710 &self.task_owner,
711 timeout,
712 )
713 .await
714 .map(|_| ())
715 }
716
717 async fn close(&mut self) -> Result<(), ConnectorError> {
718 let timeout = self
719 .config
720 .as_ref()
721 .map_or(Duration::from_secs(5), |c| c.ack_timeout);
722 let result = if let Some(Runtime::Core { client }) = self.runtime.as_ref() {
723 if self.core_dirty {
726 let result = flush_core(client, timeout, "NATS core close flush").await;
727 if result.is_ok() {
728 self.core_dirty = false;
729 }
730 result
731 } else {
732 Ok(())
733 }
734 } else {
735 drain_acks(
736 &mut self.pending_acks,
737 &self.metrics,
738 &self.task_owner,
739 timeout,
740 )
741 .await
742 .map(|_| ())
743 };
744 self.runtime = None;
745 result
746 }
747}
748
749async fn drain_acks(
754 pending: &mut VecDeque<PublishAckFuture>,
755 metrics: &NatsSinkMetrics,
756 task_owner: &ConnectorTaskOwner,
757 timeout: Duration,
758) -> Result<usize, ConnectorError> {
759 let deadline = tokio::time::Instant::now() + timeout;
760 drain_acks_until(pending, metrics, task_owner, deadline, timeout).await
761}
762
763async fn drain_acks_until(
764 pending: &mut VecDeque<PublishAckFuture>,
765 metrics: &NatsSinkMetrics,
766 task_owner: &ConnectorTaskOwner,
767 deadline: tokio::time::Instant,
768 total_timeout: Duration,
769) -> Result<usize, ConnectorError> {
770 if pending.is_empty() {
771 return Ok(0);
772 }
773 let set: FuturesUnordered<_> = pending.drain(..).map(IntoFuture::into_future).collect();
774 let task_guard = task_owner.track().ok_or_else(|| {
775 ConnectorError::Internal("NATS sink task generation is already retired".into())
776 })?;
777 let task_metrics = metrics.clone();
778 let (result_tx, result_rx) = tokio::sync::oneshot::channel();
779
780 drop(tokio::spawn(async move {
785 let _task_guard = task_guard;
786 run_ack_drain(set, task_metrics, deadline, total_timeout, result_tx).await;
787 }));
788
789 result_rx.await.map_err(|_| {
790 ConnectorError::Internal("NATS acknowledgement drain task terminated unexpectedly".into())
791 })?
792}
793
794fn retire_pending_acks(
795 pending: &mut VecDeque<PublishAckFuture>,
796 metrics: &NatsSinkMetrics,
797 task_owner: &ConnectorTaskOwner,
798) {
799 if pending.is_empty() {
800 return;
801 }
802 let count = pending.len();
803 let Some(task_guard) = task_owner.track() else {
804 for _ in 0..count {
805 metrics.record_ack_error();
806 }
807 pending.clear();
808 metrics.set_pending_futures(0);
809 return;
810 };
811 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
812 drop(task_guard);
813 for _ in 0..count {
814 metrics.record_ack_error();
815 }
816 pending.clear();
817 metrics.set_pending_futures(0);
818 return;
819 };
820
821 let mut set: FuturesUnordered<_> = pending.drain(..).map(IntoFuture::into_future).collect();
822 let metrics = metrics.clone();
823 drop(runtime.spawn(async move {
824 let _task_guard = task_guard;
825 while let Some(result) = set.next().await {
826 match result {
827 Ok(ack) if ack.duplicate => metrics.record_dedup(),
828 Ok(_) => {}
829 Err(_) => metrics.record_ack_error(),
830 }
831 }
832 metrics.set_pending_futures(0);
833 }));
834}
835
836async fn run_ack_drain<F>(
837 mut set: FuturesUnordered<F>,
838 metrics: NatsSinkMetrics,
839 deadline: tokio::time::Instant,
840 total_timeout: Duration,
841 result_tx: tokio::sync::oneshot::Sender<Result<usize, ConnectorError>>,
842) where
843 F: std::future::Future<
844 Output = Result<jetstream::publish::PublishAck, jetstream::context::PublishError>,
845 > + Send
846 + 'static,
847{
848 let mut aggregate = AckAggregate::default();
849 loop {
850 if set.is_empty() {
851 metrics.set_pending_futures(0);
852 let _ = result_tx.send(aggregate.into_result());
853 return;
854 }
855 match tokio::time::timeout_at(deadline, set.next()).await {
856 Ok(Some(Ok(ack))) => {
857 aggregate.record_applied();
858 if ack.duplicate {
859 metrics.record_dedup();
860 }
861 }
862 Ok(Some(Err(error))) => {
863 metrics.record_ack_error();
864 aggregate.record_failure(classify_jetstream_ack_failure(&error));
865 }
866 Ok(None) => {
867 metrics.set_pending_futures(0);
868 let _ = result_tx.send(aggregate.into_result());
869 return;
870 }
871 Err(_) => {
872 let unresolved = set.len();
873 for _ in 0..unresolved {
874 metrics.record_ack_error();
875 }
876 aggregate.record_unresolved_timeout(unresolved, total_timeout);
877 metrics.set_pending_futures(unresolved);
878 let _ = result_tx.send(aggregate.into_result());
879
880 while let Some(result) = set.next().await {
881 if let Ok(ack) = result {
882 if ack.duplicate {
883 metrics.record_dedup();
884 }
885 }
886 }
887 metrics.set_pending_futures(0);
888 return;
889 }
890 }
891 }
892}
893
894fn err(msg: &str) -> ConnectorError {
895 ConnectorError::ConfigurationError(msg.to_string())
896}
897
898#[cfg(test)]
899mod tests;