1use std::sync::atomic::{AtomicU32, Ordering};
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8
9use arrow_schema::SchemaRef;
10use async_nats::jetstream::{self, consumer::pull};
11use async_trait::async_trait;
12use bytes::Bytes;
13use crossfire::{mpsc, AsyncRx, MAsyncTx, TryRecvError};
14use futures_util::stream::FuturesUnordered;
15use futures_util::StreamExt;
16use tokio::sync::{mpsc as tokio_mpsc, watch, Notify};
17use tokio::task::JoinHandle;
18use tracing::{debug, warn};
19
20use super::config::{build_connect_options, AckPolicy, DeliverPolicy, Mode, NatsSourceConfig};
21use super::metrics::NatsSourceMetrics;
22use super::setup::{
23 classify_connect_error, classify_create_consumer_error, classify_get_stream_error,
24 classify_subscribe_error, track_connection_tasks,
25};
26use crate::checkpoint::SourceCheckpoint;
27use crate::config::ConnectorConfig;
28use crate::connector::{
29 ConnectorTaskGuard, ConnectorTaskOwner, ConnectorTaskTracker, SourceBatch, SourceConnector,
30 SourceConsistency, SourceContract, SourceInputMode, SourcePosition, SourceStart,
31 SourceTopology,
32};
33use crate::error::ConnectorError;
34use crate::serde::{self, RecordDeserializer};
35
36mod configuration;
37use configuration::build_pull_config;
38
39const ACK_IO_TIMEOUT: Duration = Duration::from_secs(5);
40const CLOSE_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
41const MAX_ACK_CONCURRENCY: usize = 64;
42const MAX_ACK_BACKLOG: usize = 16_384;
43
44struct Incoming {
46 payload: Bytes,
47 ack: Option<jetstream::Message>,
48}
49
50struct AckRuntime {
51 tx: Option<tokio_mpsc::Sender<jetstream::Message>>,
52 shutdown: watch::Sender<bool>,
53 task: TrackedTask,
54}
55
56struct Running {
57 deserializer: Box<dyn RecordDeserializer>,
58 rx: Option<AsyncRx<mpsc::Array<Incoming>>>,
59 shutdown: watch::Sender<bool>,
60 reader: TrackedTask,
61 ack_runtime: Option<AckRuntime>,
62}
63
64struct TrackedTask {
65 handle: Option<JoinHandle<()>>,
66 reaper_guard: Option<ConnectorTaskGuard>,
67 name: &'static str,
68}
69
70enum TaskWait {
71 Completed(Result<(), tokio::task::JoinError>),
72 TimedOut,
73}
74
75impl TrackedTask {
76 fn spawn(
77 owner: &ConnectorTaskOwner,
78 name: &'static str,
79 future: impl std::future::Future<Output = ()> + Send + 'static,
80 ) -> Result<Self, ConnectorError> {
81 let task_guard = owner.track().ok_or_else(|| {
82 ConnectorError::Internal("NATS source task generation is already retired".into())
83 })?;
84 let reaper_guard = owner.track().ok_or_else(|| {
85 ConnectorError::Internal("NATS source task generation is already retired".into())
86 })?;
87 let handle = tokio::spawn(async move {
88 let _task_guard = task_guard;
89 future.await;
90 });
91 Ok(Self {
92 handle: Some(handle),
93 reaper_guard: Some(reaper_guard),
94 name,
95 })
96 }
97
98 async fn wait_until(&mut self, deadline: tokio::time::Instant) -> TaskWait {
99 let Some(handle) = self.handle.as_mut() else {
100 return TaskWait::Completed(Ok(()));
101 };
102 match tokio::time::timeout_at(deadline, handle).await {
103 Ok(result) => {
104 self.handle.take();
105 self.reaper_guard.take();
106 TaskWait::Completed(result)
107 }
108 Err(_) => TaskWait::TimedOut,
109 }
110 }
111
112 fn retire(&mut self) {
113 let Some(handle) = self.handle.take() else {
114 self.reaper_guard.take();
115 return;
116 };
117 let reaper_guard = self.reaper_guard.take();
118 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
119 drop(handle);
122 drop(reaper_guard);
123 return;
124 };
125 let name = self.name;
126 drop(runtime.spawn(async move {
127 let _reaper_guard = reaper_guard;
128 if let Err(error) = handle.await {
129 debug!(task = name, %error, "retired NATS source task reaped");
130 }
131 }));
132 }
133}
134
135impl Drop for TrackedTask {
136 fn drop(&mut self) {
137 self.retire();
138 }
139}
140
141impl Running {
142 fn request_shutdown(&self) {
143 self.shutdown.send_replace(true);
144 }
145}
146
147impl Drop for Running {
148 fn drop(&mut self) {
149 self.request_shutdown();
152 self.rx.take();
153 if let Some(ack_runtime) = self.ack_runtime.as_mut() {
154 ack_runtime.request_shutdown();
155 ack_runtime.task.retire();
156 }
157 self.reader.retire();
158 }
159}
160
161impl AckRuntime {
162 fn spawn(
163 cfg: &NatsSourceConfig,
164 metrics: NatsSourceMetrics,
165 owner: &ConnectorTaskOwner,
166 ) -> Result<Self, ConnectorError> {
167 let (backlog, concurrency) = ack_runtime_limits(cfg);
168 let (tx, rx) = tokio_mpsc::channel(backlog);
169 let (shutdown, shutdown_rx) = watch::channel(false);
170 let task = TrackedTask::spawn(
171 owner,
172 "ack-worker",
173 run_ack_worker(rx, shutdown_rx, concurrency, metrics),
174 )?;
175 Ok(Self {
176 tx: Some(tx),
177 shutdown,
178 task,
179 })
180 }
181
182 fn request_shutdown(&mut self) {
183 self.shutdown.send_replace(true);
184 self.tx.take();
185 }
186}
187
188impl Drop for AckRuntime {
189 fn drop(&mut self) {
190 self.request_shutdown();
191 }
192}
193
194pub struct NatsSource {
196 schema: SchemaRef,
197 config: Option<NatsSourceConfig>,
198 data_ready: Arc<Notify>,
199 metrics: NatsSourceMetrics,
200 running: Option<Running>,
201 task_owner: ConnectorTaskOwner,
202 task_tracker: ConnectorTaskTracker,
203}
204
205impl NatsSource {
206 #[must_use]
208 pub fn new(schema: SchemaRef, registry: Option<&prometheus::Registry>) -> Self {
209 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
210 Self {
211 schema,
212 config: None,
213 data_ready: Arc::new(Notify::new()),
214 metrics: NatsSourceMetrics::new(registry),
215 running: None,
216 task_owner,
217 task_tracker,
218 }
219 }
220
221 #[must_use]
223 pub fn config(&self) -> Option<&NatsSourceConfig> {
224 self.config.as_ref()
225 }
226
227 #[must_use]
229 pub fn metrics_handle(&self) -> &NatsSourceMetrics {
230 &self.metrics
231 }
232
233 async fn open_jetstream(
234 &mut self,
235 cfg: &NatsSourceConfig,
236 deserializer: Box<dyn RecordDeserializer>,
237 ) -> Result<(), ConnectorError> {
238 let client = connect(cfg, &self.task_owner).await?;
239 let js = jetstream::new(client);
240
241 let stream_name = cfg
242 .stream
243 .as_deref()
244 .ok_or_else(|| err("stream name missing after validation"))?;
245 let consumer_name = cfg
246 .consumer
247 .as_deref()
248 .ok_or_else(|| err("consumer name missing after validation"))?;
249
250 let pull_cfg = build_pull_config(cfg, consumer_name)?;
251 let stream = js
252 .get_stream(stream_name)
253 .await
254 .map_err(|error| classify_get_stream_error(&error, stream_name))?;
255 let consumer = stream
256 .create_consumer(pull_cfg)
257 .await
258 .map_err(|error| classify_create_consumer_error(&error, consumer_name))?;
259
260 let (tx, rx) = mpsc::bounded_async::<Incoming>(cfg.fetch_batch * 2);
261 let (shutdown, shutdown_rx) = watch::channel(false);
262 let requires_ack = cfg.ack_policy == AckPolicy::Explicit;
263 let ack_runtime = if requires_ack {
264 Some(AckRuntime::spawn(
265 cfg,
266 self.metrics.clone(),
267 &self.task_owner,
268 )?)
269 } else {
270 None
271 };
272
273 let reader = JsReader {
274 consumer,
275 tx,
276 shutdown: shutdown_rx,
277 consecutive_errors: Arc::new(AtomicU32::new(0)),
278 data_ready: Arc::clone(&self.data_ready),
279 metrics: self.metrics.clone(),
280 batch_size: cfg.fetch_batch,
281 max_wait: cfg.fetch_max_wait,
282 lag_poll_interval: cfg.lag_poll_interval,
283 requires_ack,
284 };
285 let reader = TrackedTask::spawn(&self.task_owner, "jetstream-reader", reader.run())?;
286
287 self.running = Some(Running {
288 deserializer,
289 rx: Some(rx),
290 shutdown,
291 reader,
292 ack_runtime,
293 });
294 Ok(())
295 }
296
297 async fn open_core(
298 &mut self,
299 cfg: &NatsSourceConfig,
300 deserializer: Box<dyn RecordDeserializer>,
301 ) -> Result<(), ConnectorError> {
302 let client = connect(cfg, &self.task_owner).await?;
303 let subject = cfg
304 .subject
305 .clone()
306 .ok_or_else(|| err("subject missing after validation"))?;
307 let subscriber = if let Some(group) = cfg.queue_group.as_deref() {
308 client
309 .queue_subscribe(subject, group.to_string())
310 .await
311 .map_err(|error| classify_subscribe_error(&error, "NATS queue subscribe"))?
312 } else {
313 client
314 .subscribe(subject)
315 .await
316 .map_err(|error| classify_subscribe_error(&error, "NATS subscribe"))?
317 };
318
319 let (tx, rx) = mpsc::bounded_async::<Incoming>(cfg.fetch_batch * 2);
320 let (shutdown, shutdown_rx) = watch::channel(false);
321
322 let reader = CoreReader {
323 subscriber,
324 tx,
325 shutdown: shutdown_rx,
326 data_ready: Arc::clone(&self.data_ready),
327 };
328 let reader = TrackedTask::spawn(&self.task_owner, "core-reader", reader.run())?;
329
330 self.running = Some(Running {
331 deserializer,
332 rx: Some(rx),
333 shutdown,
334 reader,
335 ack_runtime: None,
336 });
337 Ok(())
338 }
339}
340
341#[async_trait]
342impl SourceConnector for NatsSource {
343 fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
344 Some(self.task_tracker.clone())
345 }
346
347 async fn resolve_schema(
348 &mut self,
349 config: &ConnectorConfig,
350 explicit: Option<SchemaRef>,
351 ) -> Result<crate::schema::resolution::SchemaBinding, ConnectorError> {
352 let parsed = NatsSourceConfig::from_config(config)?;
353 crate::serde::schema_contract::reader_binding(config, parsed.format, explicit)
354 }
355
356 fn contract(&self, config: &ConnectorConfig) -> Result<SourceContract, ConnectorError> {
357 let format = match config.get("format") {
358 Some(value) => serde::Format::parse(value)
359 .map_err(|error| ConnectorError::ConfigurationError(error.to_string()))?,
360 None => self
361 .config
362 .as_ref()
363 .map_or(serde::Format::Json, |config| config.format),
364 };
365 let input_mode = if format == serde::Format::Debezium {
366 SourceInputMode::KeyedUpsert
367 } else {
368 SourceInputMode::AppendOnly
369 };
370
371 Ok(SourceContract::new(
375 SourceConsistency::Ephemeral,
376 SourceTopology::Singleton,
377 input_mode,
378 ))
379 }
380
381 async fn start(&mut self, request: SourceStart) -> Result<(), ConnectorError> {
382 let (config, position, _) = request.into_parts();
383 if matches!(&position, SourcePosition::Initialized { .. }) {
384 return Err(ConnectorError::ConfigurationError(
385 "NATS has no sealed topology startup contract".into(),
386 ));
387 }
388 if let SourcePosition::Resume { attempt, .. } = position {
389 return Err(ConnectorError::ConfigurationError(format!(
390 "NATS is an ephemeral source and cannot resume checkpoint attempt {attempt:?}"
391 )));
392 }
393 let config = &config;
394
395 let cfg = NatsSourceConfig::from_config(config)?;
396 let explicit = config
399 .arrow_schema()
400 .or_else(|| (!self.schema.fields().is_empty()).then(|| self.schema.clone()));
401 let binding = crate::serde::schema_contract::reader_binding(config, cfg.format, explicit)?;
402 if config
403 .schema_binding()
404 .is_some_and(|expected| expected.logical != binding.logical)
405 {
406 return Err(ConnectorError::SchemaMismatch(
407 "NATS reader differs from its committed contract".into(),
408 ));
409 }
410 let candidate_schema = Arc::new(binding.logical);
411 let deserializer = serde::create_deserializer(cfg.format)
412 .map_err(|e| err(&format!("deserializer for format {:?}: {e}", cfg.format)))?;
413 match cfg.mode {
414 Mode::JetStream => self.open_jetstream(&cfg, deserializer).await?,
415 Mode::Core => self.open_core(&cfg, deserializer).await?,
416 }
417 self.schema = candidate_schema;
418 self.config = Some(cfg);
419 Ok(())
420 }
421
422 async fn poll_batch(
423 &mut self,
424 max_records: usize,
425 ) -> Result<Option<SourceBatch>, ConnectorError> {
426 let Some(running) = self.running.as_mut() else {
427 return Ok(None);
428 };
429
430 let mut payloads: Vec<Bytes> = Vec::new();
431 let mut new_acks: Vec<jetstream::Message> = Vec::new();
432 let mut reader_disconnected = false;
433
434 while payloads.len() < max_records {
435 let incoming = match running
436 .rx
437 .as_mut()
438 .expect("running NATS source owns its receiver")
439 .try_recv()
440 {
441 Ok(m) => m,
442 Err(TryRecvError::Empty) => break,
443 Err(TryRecvError::Disconnected) => {
444 reader_disconnected = true;
445 break;
446 }
447 };
448 payloads.push(incoming.payload);
449 if let Some(msg) = incoming.ack {
450 new_acks.push(msg);
451 }
452 }
453
454 if payloads.is_empty() {
455 if reader_disconnected {
456 return Err(ConnectorError::ReadError(
457 "NATS reader task terminated unexpectedly".into(),
458 ));
459 }
460 return Ok(None);
461 }
462
463 let records: Vec<&[u8]> = payloads.iter().map(Bytes::as_ref).collect();
464 let bytes_total: u64 = records.iter().map(|r| r.len() as u64).sum();
465 let batch = running
468 .deserializer
469 .deserialize_batch(&records, &self.schema)
470 .map_err(|e| err(&format!("deserialize batch: {e}")))?;
471
472 enqueue_acks(running.ack_runtime.as_ref(), new_acks, &self.metrics);
473
474 self.metrics
475 .record_poll(batch.num_rows() as u64, bytes_total);
476
477 Ok(Some(SourceBatch::new(batch)))
478 }
479
480 fn schema(&self) -> SchemaRef {
481 self.schema.clone()
482 }
483
484 fn checkpoint(&self) -> SourceCheckpoint {
485 SourceCheckpoint::new()
488 }
489
490 async fn close(&mut self) -> Result<(), ConnectorError> {
491 let Some(mut running) = self.running.take() else {
492 return Ok(());
493 };
494 let close_deadline = tokio::time::Instant::now() + CLOSE_DRAIN_TIMEOUT;
495 running.request_shutdown();
496 match running.reader.wait_until(close_deadline).await {
497 TaskWait::Completed(Ok(())) => {}
498 TaskWait::Completed(Err(error)) => {
499 warn!(%error, "NATS reader task failed while closing");
500 }
501 TaskWait::TimedOut => warn!(
502 "NATS reader exceeded its close deadline; its tracked reaper retains shutdown ownership"
503 ),
504 }
505 running.rx.take();
508
509 if let Some(mut ack_runtime) = running.ack_runtime.take() {
510 ack_runtime.request_shutdown();
511 match ack_runtime.task.wait_until(close_deadline).await {
512 TaskWait::Completed(Ok(())) => {}
513 TaskWait::Completed(Err(error)) => {
514 warn!(%error, "NATS ack worker failed while closing");
515 self.metrics.record_abandoned_acks();
516 }
517 TaskWait::TimedOut => {
518 warn!(
519 "NATS ack worker exceeded its close deadline; its tracked reaper retains shutdown ownership"
520 );
521 }
522 }
523 }
524 Ok(())
525 }
526
527 fn data_ready_notify(&self) -> Option<Arc<Notify>> {
528 Some(Arc::clone(&self.data_ready))
529 }
530}
531
532fn err(msg: &str) -> ConnectorError {
535 ConnectorError::ConfigurationError(msg.to_string())
536}
537
538fn ack_runtime_limits(cfg: &NatsSourceConfig) -> (usize, usize) {
539 let broker_limit = usize::try_from(cfg.max_ack_pending)
540 .ok()
541 .filter(|limit| *limit > 0);
542 let fallback = cfg.fetch_batch.saturating_mul(2);
543 let backlog = broker_limit.unwrap_or(fallback).clamp(1, MAX_ACK_BACKLOG);
544 let concurrency = cfg.fetch_batch.clamp(1, MAX_ACK_CONCURRENCY).min(backlog);
545 (backlog, concurrency)
546}
547
548fn enqueue_acks(
549 runtime: Option<&AckRuntime>,
550 messages: Vec<jetstream::Message>,
551 metrics: &NatsSourceMetrics,
552) {
553 if messages.is_empty() {
554 return;
555 }
556 let Some(runtime) = runtime else {
557 metrics.record_ack_enqueue_errors(messages.len());
558 warn!(
559 rejected = messages.len(),
560 "JetStream ack worker is unavailable; broker will redeliver"
561 );
562 return;
563 };
564
565 let mut rejected = 0usize;
566 for message in messages {
567 metrics.record_ack_enqueued();
569 if runtime
570 .tx
571 .as_ref()
572 .is_none_or(|tx| tx.try_send(message).is_err())
573 {
574 metrics.record_ack_error();
575 rejected += 1;
576 }
577 }
578 if rejected > 0 {
579 warn!(
580 rejected,
581 "JetStream ack backlog is full or closed; broker will redeliver"
582 );
583 }
584}
585
586async fn run_ack_worker(
587 rx: tokio_mpsc::Receiver<jetstream::Message>,
588 shutdown: watch::Receiver<bool>,
589 concurrency: usize,
590 metrics: NatsSourceMetrics,
591) {
592 let task_metrics = metrics.clone();
595 let abandoned = run_bounded_queue(rx, shutdown, concurrency, move |message| {
596 let metrics = task_metrics.clone();
597 async move {
598 acknowledge_message(message, &metrics).await;
599 }
600 })
601 .await;
602 if abandoned > 0 {
603 metrics.record_ack_abandoned(abandoned);
604 warn!(
605 abandoned,
606 "discarded queued JetStream acknowledgements during shutdown; broker will redeliver"
607 );
608 }
609}
610
611async fn run_bounded_queue<T, F, Fut>(
612 mut rx: tokio_mpsc::Receiver<T>,
613 mut shutdown: watch::Receiver<bool>,
614 concurrency: usize,
615 process: F,
616) -> usize
617where
618 T: Send + 'static,
619 F: Fn(T) -> Fut,
620 Fut: std::future::Future<Output = ()> + Send,
621{
622 debug_assert!(concurrency > 0);
623 let mut in_flight = FuturesUnordered::new();
624 'input: loop {
625 if shutdown_requested(&shutdown) || rx.is_closed() {
626 break;
627 }
628 while in_flight.len() >= concurrency {
629 tokio::select! {
630 biased;
631 _ = shutdown.changed() => break 'input,
632 _ = in_flight.next() => {}
633 }
634 if rx.is_closed() {
635 break 'input;
636 }
637 }
638
639 tokio::select! {
640 biased;
641 _ = shutdown.changed() => break,
642 _ = in_flight.next(), if !in_flight.is_empty() => {}
643 message = rx.recv() => {
644 let Some(message) = message else {
645 break;
646 };
647 in_flight.push(process(message));
648 }
649 }
650 }
651
652 rx.close();
655 let mut abandoned = 0usize;
656 while rx.try_recv().is_ok() {
657 abandoned = abandoned.saturating_add(1);
658 }
659 while in_flight.next().await.is_some() {}
660 abandoned
661}
662
663async fn acknowledge_message(message: jetstream::Message, metrics: &NatsSourceMetrics) {
664 match tokio::time::timeout(ACK_IO_TIMEOUT, message.double_ack()).await {
665 Ok(Ok(())) => metrics.record_ack(),
666 Ok(Err(error)) => {
667 metrics.record_ack_error();
668 warn!(%error, "JetStream ack failed; broker will redeliver");
669 }
670 Err(_) => {
671 metrics.record_ack_error();
672 warn!(
673 timeout_ms = ACK_IO_TIMEOUT.as_millis(),
674 "JetStream ack timed out; broker will redeliver"
675 );
676 }
677 }
678}
679
680async fn connect(
681 cfg: &NatsSourceConfig,
682 owner: &ConnectorTaskOwner,
683) -> Result<async_nats::Client, ConnectorError> {
684 track_connection_tasks(build_connect_options(&cfg.auth, &cfg.tls)?, owner, "source")?
685 .connect(&cfg.servers)
686 .await
687 .map_err(|error| classify_connect_error(&error))
688}
689
690fn map_ack_policy(p: AckPolicy) -> async_nats::jetstream::consumer::AckPolicy {
691 use async_nats::jetstream::consumer::AckPolicy as Nats;
692 match p {
693 AckPolicy::Explicit => Nats::Explicit,
694 AckPolicy::None => Nats::None,
695 }
696}
697
698fn fetch_backoff_base(consecutive_errors: u32) -> Duration {
700 let exp = consecutive_errors.saturating_sub(1).min(4);
701 let ms = 500u64.saturating_mul(1u64 << exp);
702 Duration::from_millis(ms.min(5000))
703}
704
705fn with_jitter(base: Duration, entropy: u64) -> Duration {
707 let base_ms = u64::try_from(base.as_millis()).unwrap_or(u64::MAX);
708 let range = (base_ms / 5).max(1); let window = range * 2 + 1;
710 let offset = entropy % window;
711 let jittered = base_ms.saturating_add(offset).saturating_sub(range);
712 Duration::from_millis(jittered)
713}
714
715fn fetch_backoff(consecutive_errors: u32, entropy: u64) -> Duration {
716 with_jitter(fetch_backoff_base(consecutive_errors), entropy)
717}
718
719#[allow(clippy::cast_possible_truncation)]
722fn entropy_now() -> u64 {
723 std::time::SystemTime::now()
724 .duration_since(std::time::UNIX_EPOCH)
725 .unwrap_or_default()
726 .as_nanos() as u64
727}
728
729fn shutdown_requested(shutdown: &watch::Receiver<bool>) -> bool {
730 *shutdown.borrow() || shutdown.has_changed().is_err()
731}
732
733struct JsReader {
734 consumer: jetstream::consumer::Consumer<pull::Config>,
735 tx: MAsyncTx<mpsc::Array<Incoming>>,
736 shutdown: watch::Receiver<bool>,
737 consecutive_errors: Arc<AtomicU32>,
738 data_ready: Arc<Notify>,
739 metrics: NatsSourceMetrics,
740 batch_size: usize,
741 max_wait: Duration,
742 lag_poll_interval: Duration,
744 requires_ack: bool,
745}
746
747impl JsReader {
748 async fn run(self) {
749 let Self {
750 mut consumer,
751 tx,
752 mut shutdown,
753 consecutive_errors,
754 data_ready,
755 metrics,
756 batch_size,
757 max_wait,
758 lag_poll_interval,
759 requires_ack,
760 } = self;
761
762 let mut last_lag_poll = Instant::now();
763 let lag_poll_enabled = !lag_poll_interval.is_zero();
764
765 loop {
766 if shutdown_requested(&shutdown) {
767 break;
768 }
769 let fetch_result = tokio::select! {
770 biased;
771 _ = shutdown.changed() => break,
772 r = consumer.fetch().max_messages(batch_size).expires(max_wait).messages() => r,
773 };
774
775 let mut stream = match fetch_result {
776 Ok(s) => s,
777 Err(e) => {
778 let errs = consecutive_errors.fetch_add(1, Ordering::AcqRel) + 1;
779 metrics.record_fetch_error();
780 warn!(
781 error = %e,
782 consecutive_errors = errs,
783 "nats fetch() errored; backing off",
784 );
785 let backoff = fetch_backoff(errs, entropy_now());
786 tokio::select! {
787 biased;
788 _ = shutdown.changed() => break,
789 () = tokio::time::sleep(backoff) => {}
790 }
791 continue;
792 }
793 };
794
795 let mut forwarded = 0usize;
796 let mut stream_errors = 0usize;
797 loop {
798 let msg_result = tokio::select! {
799 biased;
800 _ = shutdown.changed() => return,
801 r = stream.next() => match r {
802 Some(r) => r,
803 None => break,
804 },
805 };
806 let msg = match msg_result {
807 Ok(m) => m,
808 Err(e) => {
809 metrics.record_fetch_error();
810 stream_errors += 1;
811 warn!(error = %e, "nats message error");
812 continue;
813 }
814 };
815 let payload = msg.payload.clone();
816 let incoming = Incoming {
817 payload,
818 ack: requires_ack.then_some(msg),
819 };
820 let send_result = tokio::select! {
821 biased;
822 _ = shutdown.changed() => return,
823 result = tx.send(incoming) => result,
824 };
825 if send_result.is_err() {
826 debug!("nats reader: downstream channel closed");
827 return;
828 }
829 forwarded += 1;
830 }
831
832 if forwarded > 0 {
835 consecutive_errors.store(0, Ordering::Release);
836 data_ready.notify_one();
837 } else if stream_errors > 0 {
838 let errs = consecutive_errors.fetch_add(1, Ordering::AcqRel) + 1;
839 let backoff = fetch_backoff(errs, entropy_now());
840 tokio::select! {
841 biased;
842 _ = shutdown.changed() => break,
843 () = tokio::time::sleep(backoff) => {}
844 }
845 }
846
847 if lag_poll_enabled && last_lag_poll.elapsed() >= lag_poll_interval {
848 last_lag_poll = Instant::now();
849 match consumer.info().await {
850 Ok(info) => metrics.set_consumer_lag(info.num_pending),
851 Err(e) => warn!(error = %e, "consumer.info() failed; skipping lag update"),
852 }
853 }
854 }
855 }
856}
857
858struct CoreReader {
859 subscriber: async_nats::Subscriber,
860 tx: MAsyncTx<mpsc::Array<Incoming>>,
861 shutdown: watch::Receiver<bool>,
862 data_ready: Arc<Notify>,
863}
864
865impl CoreReader {
866 async fn run(self) {
867 let Self {
868 mut subscriber,
869 tx,
870 mut shutdown,
871 data_ready,
872 } = self;
873
874 loop {
875 if shutdown_requested(&shutdown) {
876 break;
877 }
878 let msg = tokio::select! {
879 biased;
880 _ = shutdown.changed() => break,
881 m = subscriber.next() => match m {
882 Some(m) => m,
883 None => break,
884 },
885 };
886 let incoming = Incoming {
887 payload: msg.payload,
888 ack: None,
889 };
890 let send_result = tokio::select! {
891 biased;
892 _ = shutdown.changed() => break,
893 result = tx.send(incoming) => result,
894 };
895 if send_result.is_err() {
896 break;
897 }
898 data_ready.notify_one();
899 }
900 data_ready.notify_one();
903 }
904}
905
906#[cfg(test)]
907mod tests;