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