Skip to main content

dynamo_runtime/transports/
nats.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! NATS transport
5//!
6//! The following environment variables are used to configure the NATS client:
7//!
8//! - `NATS_SERVER`: the NATS server address
9//!
10//! For authentication, the following environment variables are used and prioritized in the following order:
11//!
12//! - `NATS_AUTH_USERNAME`: the username for authentication
13//! - `NATS_AUTH_PASSWORD`: the password for authentication
14//! - `NATS_AUTH_TOKEN`: the token for authentication
15//! - `NATS_AUTH_NKEY`: the nkey for authentication
16//! - `NATS_AUTH_CREDENTIALS_FILE`: the path to the credentials file
17//!
18//! Note: `NATS_AUTH_USERNAME` and `NATS_AUTH_PASSWORD` must be used together.
19//!
20//! ## TLS
21//!
22//! A custom TLS config is applied when `NATS_TLS_CA_CERT_PATH` is set,
23//! `NATS_TLS_INSECURE` is truthy, or a client certificate is configured. When
24//! only the `tls://` URL scheme is used without explicit TLS env vars,
25//! async-nats handles TLS natively with system roots.
26//!
27//! - `NATS_TLS_CA_CERT_PATH`: path to the CA cert PEM used to verify the server
28//! - `NATS_TLS_CLIENT_CERT_PATH`: client cert PEM for mutual TLS (optional)
29//! - `NATS_TLS_CLIENT_KEY_PATH`: client key PEM for mutual TLS (optional)
30//! - `NATS_TLS_INSECURE`: set to a truthy value to skip certificate verification (dev only)
31use crate::metrics::MetricsHierarchy;
32use crate::protocols::EndpointId;
33
34use anyhow::{Context, Result};
35use async_nats::connection::State;
36use async_nats::{ConnectError, ConnectErrorKind, ConnectOptions, Subscriber, client, jetstream};
37use async_trait::async_trait;
38use bytes::Bytes;
39use derive_builder::Builder;
40use futures::{StreamExt, TryStreamExt};
41use prometheus::{Counter, Gauge, Histogram, HistogramOpts, IntCounter, IntGauge, Opts, Registry};
42use serde::de::DeserializeOwned;
43use serde::{Deserialize, Serialize};
44use std::path::{Path, PathBuf};
45use std::sync::atomic::Ordering;
46use std::time::Duration;
47use tokio::fs::File as TokioFile;
48use tokio::io::AsyncRead;
49use tokio::time;
50use tokio_util::sync::CancellationToken;
51use url::Url;
52use validator::{Validate, ValidationError};
53
54use crate::config::environment_names::nats as env_nats;
55pub use crate::slug::Slug;
56use tracing as log;
57
58use super::utils::build_in_runtime;
59
60pub const URL_PREFIX: &str = "nats://";
61
62#[derive(Clone)]
63pub struct Client {
64    client: client::Client,
65    js_ctx: jetstream::Context,
66}
67
68impl Client {
69    /// Create a NATS [`ClientOptionsBuilder`].
70    pub fn builder() -> ClientOptionsBuilder {
71        ClientOptionsBuilder::default()
72    }
73
74    /// Returns a reference to the underlying [`async_nats::client::Client`] instance
75    pub fn client(&self) -> &client::Client {
76        &self.client
77    }
78
79    /// Returns a reference to the underlying [`async_nats::jetstream::Context`] instance
80    pub fn jetstream(&self) -> &jetstream::Context {
81        &self.js_ctx
82    }
83
84    /// host:port of NATS
85    pub fn addr(&self) -> String {
86        let info = self.client.server_info();
87        format!("{}:{}", info.host, info.port)
88    }
89
90    /// fetch the list of streams
91    pub async fn list_streams(&self) -> Result<Vec<String>> {
92        let names = self.js_ctx.stream_names();
93        let stream_names: Vec<String> = names.try_collect().await?;
94        Ok(stream_names)
95    }
96
97    /// fetch the list of consumers for a given stream
98    pub async fn list_consumers(&self, stream_name: &str) -> Result<Vec<String>> {
99        let stream = self.js_ctx.get_stream(stream_name).await?;
100        let consumers: Vec<String> = stream.consumer_names().try_collect().await?;
101        Ok(consumers)
102    }
103
104    pub async fn stream_info(&self, stream_name: &str) -> Result<jetstream::stream::State> {
105        let mut stream = self.js_ctx.get_stream(stream_name).await?;
106        let info = stream.info().await?;
107        Ok(info.state.clone())
108    }
109
110    pub async fn get_stream(&self, name: &str) -> Result<jetstream::stream::Stream> {
111        let stream = self.js_ctx.get_stream(name).await?;
112        Ok(stream)
113    }
114
115    /// Issues a broadcast request for all services with the provided `service_name` to report their
116    /// current stats. Each service will only respond once. The service may have customized the reply
117    /// so the caller should select which endpoint and what concrete data model should be used to
118    /// extract the details.
119    ///
120    /// Note: Because each endpoint will only reply once, the caller must drop the subscription after
121    /// some time or it will await forever.
122    pub async fn scrape_service(&self, service_name: &str) -> Result<Subscriber> {
123        let subject = format!("$SRV.STATS.{}", service_name);
124        let reply_subject = format!("_INBOX.{}", nuid::next());
125        let subscription = self.client.subscribe(reply_subject.clone()).await?;
126
127        // Publish the request with the reply-to subject
128        self.client
129            .publish_with_reply(subject, reply_subject, "".into())
130            .await?;
131
132        Ok(subscription)
133    }
134
135    /// Helper method to get or optionally create an object store bucket
136    ///
137    /// # Arguments
138    /// * `bucket_name` - The name of the bucket to retrieve
139    /// * `create_if_not_found` - If true, creates the bucket when it doesn't exist
140    ///
141    /// # Returns
142    /// The object store bucket or an error
143    async fn get_or_create_bucket(
144        &self,
145        bucket_name: &str,
146        create_if_not_found: bool,
147    ) -> anyhow::Result<jetstream::object_store::ObjectStore> {
148        let context = self.jetstream();
149
150        match context.get_object_store(bucket_name).await {
151            Ok(bucket) => Ok(bucket),
152            Err(err) if err.to_string().contains("stream not found") => {
153                // err.source() is GetStreamError, which has a kind() which
154                // is GetStreamErrorKind::JetStream which wraps a jetstream::Error
155                // which has code 404. Phew. So yeah check the string for now.
156
157                if create_if_not_found {
158                    tracing::debug!("Creating NATS bucket {bucket_name}");
159                    context
160                        .create_object_store(jetstream::object_store::Config {
161                            bucket: bucket_name.to_string(),
162                            ..Default::default()
163                        })
164                        .await
165                        .map_err(|e| anyhow::anyhow!("Failed creating bucket / object store: {e}"))
166                } else {
167                    anyhow::bail!(
168                        "NATS get_object_store bucket does not exist: {bucket_name}. {err}."
169                    );
170                }
171            }
172            Err(err) => {
173                anyhow::bail!("NATS get_object_store error: {err}");
174            }
175        }
176    }
177
178    /// Upload file to NATS at this URL
179    pub async fn object_store_upload(&self, filepath: &Path, nats_url: &Url) -> anyhow::Result<()> {
180        let mut disk_file = TokioFile::open(filepath).await?;
181
182        let (bucket_name, key) = url_to_bucket_and_key(nats_url)?;
183        let bucket = self.get_or_create_bucket(&bucket_name, true).await?;
184
185        let key_meta = async_nats::jetstream::object_store::ObjectMetadata {
186            name: key.to_string(),
187            ..Default::default()
188        };
189        bucket.put(key_meta, &mut disk_file).await.map_err(|e| {
190            anyhow::anyhow!("Failed uploading to bucket / object store {bucket_name}/{key}: {e}")
191        })?;
192
193        Ok(())
194    }
195
196    /// Download file from NATS at this URL
197    pub async fn object_store_download(
198        &self,
199        nats_url: &Url,
200        filepath: &Path,
201    ) -> anyhow::Result<()> {
202        let mut disk_file = TokioFile::create(filepath).await?;
203
204        let (bucket_name, key) = url_to_bucket_and_key(nats_url)?;
205        let bucket = self.get_or_create_bucket(&bucket_name, false).await?;
206
207        let mut obj_reader = bucket.get(&key).await.map_err(|e| {
208            anyhow::anyhow!(
209                "Failed downloading from bucket / object store {bucket_name}/{key}: {e}"
210            )
211        })?;
212        let _bytes_copied = tokio::io::copy(&mut obj_reader, &mut disk_file).await?;
213
214        Ok(())
215    }
216
217    /// Delete a bucket and all it's contents from the NATS object store
218    pub async fn object_store_delete_bucket(&self, bucket_name: &str) -> anyhow::Result<()> {
219        let context = self.jetstream();
220        match context.delete_object_store(&bucket_name).await {
221            Ok(_) => Ok(()),
222            Err(err) if err.to_string().contains("stream not found") => {
223                tracing::trace!(bucket_name, "NATS bucket already gone");
224                Ok(())
225            }
226            Err(err) => Err(anyhow::anyhow!("NATS get_object_store error: {err}")),
227        }
228    }
229
230    /// Upload a serializable struct to NATS object store using bincode
231    pub async fn object_store_upload_data<T>(&self, data: &T, nats_url: &Url) -> anyhow::Result<()>
232    where
233        T: Serialize,
234    {
235        // Serialize the data using bincode (more efficient binary format)
236        let binary_data = bincode::serialize(data)
237            .map_err(|e| anyhow::anyhow!("Failed to serialize data with bincode: {e}"))?;
238
239        let (bucket_name, key) = url_to_bucket_and_key(nats_url)?;
240        let bucket = self.get_or_create_bucket(&bucket_name, true).await?;
241
242        let key_meta = async_nats::jetstream::object_store::ObjectMetadata {
243            name: key.to_string(),
244            ..Default::default()
245        };
246
247        // Upload the serialized bytes
248        let mut cursor = std::io::Cursor::new(binary_data);
249        bucket.put(key_meta, &mut cursor).await.map_err(|e| {
250            anyhow::anyhow!("Failed uploading to bucket / object store {bucket_name}/{key}: {e}")
251        })?;
252
253        Ok(())
254    }
255
256    /// Download and deserialize a struct from NATS object store using bincode
257    pub async fn object_store_download_data<T>(&self, nats_url: &Url) -> anyhow::Result<T>
258    where
259        T: DeserializeOwned,
260    {
261        let (bucket_name, key) = url_to_bucket_and_key(nats_url)?;
262        let bucket = self.get_or_create_bucket(&bucket_name, false).await?;
263
264        let mut obj_reader = bucket.get(&key).await.map_err(|e| {
265            anyhow::anyhow!(
266                "Failed downloading from bucket / object store {bucket_name}/{key}: {e}"
267            )
268        })?;
269
270        // Read all bytes into memory
271        let mut buffer = Vec::new();
272        tokio::io::copy(&mut obj_reader, &mut buffer)
273            .await
274            .map_err(|e| anyhow::anyhow!("Failed reading object data: {e}"))?;
275        tracing::debug!("Downloaded {} bytes from {bucket_name}/{key}", buffer.len());
276
277        // Deserialize from bincode
278        let data = bincode::deserialize(&buffer)
279            .map_err(|e| anyhow::anyhow!("Failed to deserialize data with bincode: {e}"))?;
280
281        Ok(data)
282    }
283}
284
285/// NATS client options
286///
287/// This object uses the builder pattern with default values that are evaluates
288/// from the environment variables if they are not explicitly set by the builder.
289#[derive(Debug, Clone, Builder, Validate)]
290pub struct ClientOptions {
291    #[builder(setter(into), default = "default_server()")]
292    #[validate(custom(function = "validate_nats_server"))]
293    server: String,
294
295    #[builder(default)]
296    auth: NatsAuth,
297
298    #[builder(default = "default_startup_connect_timeout()")]
299    startup_connect_timeout: Duration,
300
301    /// Path to PEM CA certificate for TLS. When set, TLS is required and
302    /// `NATS_SERVER` must use the `tls://` scheme.
303    #[builder(default = "default_nats_tls_ca_cert_path()")]
304    tls_ca_cert_path: Option<PathBuf>,
305
306    /// Path to PEM client certificate presented to the NATS server for mutual
307    /// TLS (mTLS). Must be set together with `tls_client_key_path`.
308    #[builder(default = "default_nats_tls_client_cert_path()")]
309    tls_client_cert_path: Option<PathBuf>,
310
311    /// Path to PEM client private key for mutual TLS (mTLS).
312    #[builder(default = "default_nats_tls_client_key_path()")]
313    tls_client_key_path: Option<PathBuf>,
314
315    /// Skip TLS certificate verification. For development only.
316    #[builder(default = "default_nats_tls_insecure()")]
317    tls_insecure: bool,
318}
319
320fn default_server() -> String {
321    if let Ok(server) = std::env::var(env_nats::NATS_SERVER) {
322        return server;
323    }
324
325    "nats://localhost:4222".to_string()
326}
327
328fn validate_nats_server(server: &str) -> Result<(), ValidationError> {
329    if server.starts_with("nats://") || server.starts_with("tls://") {
330        Ok(())
331    } else {
332        Err(ValidationError::new(
333            "server must start with 'nats://' or 'tls://'",
334        ))
335    }
336}
337
338fn default_nats_tls_ca_cert_path() -> Option<PathBuf> {
339    std::env::var(env_nats::tls::NATS_TLS_CA_CERT_PATH)
340        .ok()
341        .map(PathBuf::from)
342}
343
344fn default_nats_tls_client_cert_path() -> Option<PathBuf> {
345    std::env::var(env_nats::tls::NATS_TLS_CLIENT_CERT_PATH)
346        .ok()
347        .map(PathBuf::from)
348}
349
350fn default_nats_tls_client_key_path() -> Option<PathBuf> {
351    std::env::var(env_nats::tls::NATS_TLS_CLIENT_KEY_PATH)
352        .ok()
353        .map(PathBuf::from)
354}
355
356fn default_nats_tls_insecure() -> bool {
357    crate::config::env_is_truthy(env_nats::tls::NATS_TLS_INSECURE)
358}
359
360// TODO(jthomson04): We really shouldn't be hardcoding this.
361const NATS_WORKER_THREADS: usize = 4;
362
363const DEFAULT_STARTUP_CONNECT_TIMEOUT: Duration = Duration::from_secs(120);
364const STARTUP_CONNECT_INITIAL_BACKOFF: Duration = Duration::from_secs(1);
365const STARTUP_CONNECT_MAX_BACKOFF: Duration = Duration::from_secs(5);
366const STARTUP_CONNECT_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(5);
367
368fn startup_connect_timeout_from_value(value: Option<&str>) -> Duration {
369    match value {
370        Some(raw) => match raw.parse::<u64>() {
371            Ok(seconds) if seconds > 0 => Duration::from_secs(seconds),
372            Ok(_) => {
373                tracing::warn!(
374                    "{} must be >= 1; got 0. Falling back to {}.",
375                    env_nats::NATS_STARTUP_CONNECT_TIMEOUT_SECONDS,
376                    DEFAULT_STARTUP_CONNECT_TIMEOUT.as_secs()
377                );
378                DEFAULT_STARTUP_CONNECT_TIMEOUT
379            }
380            Err(error) => {
381                tracing::warn!(
382                    "Invalid {}='{}' ({error}). Falling back to {}.",
383                    env_nats::NATS_STARTUP_CONNECT_TIMEOUT_SECONDS,
384                    raw,
385                    DEFAULT_STARTUP_CONNECT_TIMEOUT.as_secs()
386                );
387                DEFAULT_STARTUP_CONNECT_TIMEOUT
388            }
389        },
390        None => DEFAULT_STARTUP_CONNECT_TIMEOUT,
391    }
392}
393
394fn default_startup_connect_timeout() -> Duration {
395    startup_connect_timeout_from_value(
396        std::env::var(env_nats::NATS_STARTUP_CONNECT_TIMEOUT_SECONDS)
397            .ok()
398            .as_deref(),
399    )
400}
401
402fn is_retryable_connect_error(kind: ConnectErrorKind) -> bool {
403    matches!(
404        kind,
405        ConnectErrorKind::Dns | ConnectErrorKind::Io | ConnectErrorKind::TimedOut
406    )
407}
408
409async fn connect_with_startup_retry(
410    options: ConnectOptions,
411    server: String,
412    timeout: Duration,
413    token: CancellationToken,
414) -> Result<async_nats::Client> {
415    let deadline = time::Instant::now()
416        .checked_add(timeout)
417        .context("NATS startup connection timeout exceeds the supported duration")?;
418    let mut backoff = STARTUP_CONNECT_INITIAL_BACKOFF;
419    let mut attempts = 0;
420    let mut last_error: Option<anyhow::Error> = None;
421
422    loop {
423        if token.is_cancelled() {
424            anyhow::bail!("NATS startup connection cancelled");
425        }
426        let remaining = deadline.saturating_duration_since(time::Instant::now());
427        if remaining.is_zero() {
428            let message = format!(
429                "NATS startup connection timed out after {} seconds ({attempts} attempts)",
430                timeout.as_secs_f64()
431            );
432            return Err(match last_error {
433                Some(error) => error.context(message),
434                None => anyhow::anyhow!(message),
435            });
436        }
437
438        attempts += 1;
439        let attempt = tokio::select! {
440            biased;
441
442            _ = token.cancelled() => {
443                anyhow::bail!("NATS startup connection cancelled");
444            }
445            result = time::timeout(
446                remaining.min(STARTUP_CONNECT_ATTEMPT_TIMEOUT),
447                options.clone().connect(server.as_str()),
448            ) => match result {
449                Ok(result) => result,
450                Err(error) => Err(ConnectError::with_source(ConnectErrorKind::TimedOut, error)),
451            },
452        };
453
454        let error = match attempt {
455            Ok(client) => {
456                if attempts > 1 {
457                    tracing::info!(attempts, "NATS startup connection established after retry");
458                }
459                return Ok(client);
460            }
461            Err(error) => error,
462        };
463        let kind = error.kind();
464        if !is_retryable_connect_error(kind) {
465            return Err(anyhow::Error::new(error).context("Failed to connect to NATS"));
466        }
467        last_error = Some(anyhow::Error::new(error));
468        let remaining = deadline.saturating_duration_since(time::Instant::now());
469        let delay = backoff.min(remaining);
470        tracing::warn!(
471            attempt = attempts,
472            error_kind = ?kind,
473            remaining = ?remaining,
474            retry_in = ?delay,
475            "NATS not reachable yet; retrying startup connection"
476        );
477
478        tokio::select! {
479            biased;
480
481            _ = token.cancelled() => {
482                anyhow::bail!("NATS startup connection cancelled");
483            }
484            _ = time::sleep(delay) => {}
485        }
486        backoff = backoff.saturating_mul(2).min(STARTUP_CONNECT_MAX_BACKOFF);
487    }
488}
489
490impl ClientOptions {
491    /// Create a new [`ClientOptionsBuilder`]
492    pub fn builder() -> ClientOptionsBuilder {
493        ClientOptionsBuilder::default()
494    }
495
496    /// Validate the config and connect to NATS within the startup timeout.
497    pub async fn connect(self) -> Result<Client> {
498        self.connect_with_cancellation(CancellationToken::new())
499            .await
500    }
501
502    /// Connect to NATS within the startup timeout, stopping when the token is cancelled.
503    pub async fn connect_with_cancellation(self, token: CancellationToken) -> Result<Client> {
504        self.validate()?;
505
506        // Client cert and key must be set together to present a client identity.
507        if self.tls_client_cert_path.is_some() != self.tls_client_key_path.is_some() {
508            anyhow::bail!(
509                "Both {} and {} must be set together to enable NATS mTLS",
510                env_nats::tls::NATS_TLS_CLIENT_CERT_PATH,
511                env_nats::tls::NATS_TLS_CLIENT_KEY_PATH,
512            );
513        }
514
515        // A client identity requires a CA (or insecure) so the server can still
516        // be verified; otherwise the root store would be empty and verification
517        // would fail with an opaque error.
518        if self.tls_client_cert_path.is_some()
519            && self.tls_ca_cert_path.is_none()
520            && !self.tls_insecure
521        {
522            anyhow::bail!(
523                "{} requires {} (or {}) to also be set",
524                env_nats::tls::NATS_TLS_CLIENT_CERT_PATH,
525                env_nats::tls::NATS_TLS_CA_CERT_PATH,
526                env_nats::tls::NATS_TLS_INSECURE,
527            );
528        }
529
530        let custom_tls = self.tls_ca_cert_path.is_some()
531            || self.tls_insecure
532            || self.tls_client_cert_path.is_some();
533        let tls_url = self.server.starts_with("tls://");
534
535        // Custom TLS settings imply an encrypted connection, so the server URL
536        // must use the tls:// scheme. Reject the mismatch up front with a clear
537        // error instead of silently forcing TLS onto a nats:// URL.
538        if custom_tls && !tls_url {
539            anyhow::bail!(
540                "NATS TLS is configured (NATS_TLS_CA_CERT_PATH, NATS_TLS_INSECURE, or a client \
541                 certificate) but NATS_SERVER does not use the 'tls://' scheme: {}",
542                self.server
543            );
544        }
545
546        let mut options = match self.auth {
547            NatsAuth::UserPass(username, password) => {
548                async_nats::ConnectOptions::with_user_and_password(username, password)
549            }
550            NatsAuth::Token(token) => async_nats::ConnectOptions::with_token(token),
551            NatsAuth::NKey(nkey) => async_nats::ConnectOptions::with_nkey(nkey),
552            NatsAuth::CredentialsFile(path) => {
553                async_nats::ConnectOptions::with_credentials_file(path).await?
554            }
555        };
556
557        // Install the ring crypto provider as the process-level default, but
558        // only when this connection actually uses TLS. async-nats calls
559        // ClientConfig::builder() internally for a tls:// URL and panics if no
560        // provider is installed; both ring and aws-lc-rs are compiled in (via
561        // async-nats and kube respectively), so rustls 0.23 cannot auto-detect.
562        // Gating on TLS avoids clobbering another component's provider choice
563        // (e.g. the HTTPS frontend's aws-lc-rs) for plaintext nats:// use.
564        // Silently ignored if a provider is already installed.
565        if custom_tls || tls_url {
566            let _ = rustls::crypto::ring::default_provider().install_default();
567        }
568
569        // Apply a custom TLS config when a CA cert or insecure mode is explicitly
570        // configured. When only a tls:// URL is used without explicit TLS env vars,
571        // let async-nats handle TLS natively (it uses its own rustls setup with
572        // system roots).
573        if custom_tls {
574            let tls_config = crate::tls_utils::client_tls_config(
575                self.tls_ca_cert_path.as_deref(),
576                self.tls_insecure,
577                self.tls_client_cert_path.as_deref(),
578                self.tls_client_key_path.as_deref(),
579            )?;
580            options = options.tls_client_config(tls_config).require_tls(true);
581        } else if tls_url {
582            // tls:// URL implies TLS but no custom CA — async-nats will use its
583            // built-in rustls with system roots. Just require TLS on the connection.
584            options = options.require_tls(true);
585        }
586
587        // 0 is treated as unset — Duration::from_secs(0) would time out every request immediately.
588        let request_timeout = std::env::var(env_nats::DYN_NATS_REQUEST_TIMEOUT_SECS)
589            .ok()
590            .and_then(|v| v.parse::<u64>().ok())
591            .filter(|&secs| secs > 0)
592            .map(time::Duration::from_secs);
593        let options = match request_timeout {
594            Some(timeout) => options.request_timeout(Some(timeout)),
595            None => options,
596        };
597
598        // async-nats retains this timeout for steady-state reconnect attempts too.
599        let options = options.connection_timeout(STARTUP_CONNECT_ATTEMPT_TIMEOUT);
600        let (client, _) = build_in_runtime(
601            connect_with_startup_retry(options, self.server, self.startup_connect_timeout, token),
602            NATS_WORKER_THREADS,
603        )
604        .await?;
605
606        let js_ctx = jetstream::new(client.clone());
607
608        Ok(Client { client, js_ctx })
609    }
610}
611
612impl Default for ClientOptions {
613    fn default() -> Self {
614        ClientOptions {
615            server: default_server(),
616            auth: NatsAuth::default(),
617            startup_connect_timeout: default_startup_connect_timeout(),
618            tls_ca_cert_path: default_nats_tls_ca_cert_path(),
619            tls_client_cert_path: default_nats_tls_client_cert_path(),
620            tls_client_key_path: default_nats_tls_client_key_path(),
621            tls_insecure: default_nats_tls_insecure(),
622        }
623    }
624}
625
626#[derive(Clone, Eq, PartialEq)]
627pub enum NatsAuth {
628    UserPass(String, String),
629    Token(String),
630    NKey(String),
631    CredentialsFile(PathBuf),
632}
633
634impl std::fmt::Debug for NatsAuth {
635    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636        match self {
637            NatsAuth::UserPass(user, _pass) => {
638                write!(f, "UserPass({}, <redacted>)", user)
639            }
640            NatsAuth::Token(_token) => write!(f, "Token(<redacted>)"),
641            NatsAuth::NKey(_nkey) => write!(f, "NKey(<redacted>)"),
642            NatsAuth::CredentialsFile(path) => write!(f, "CredentialsFile({:?})", path),
643        }
644    }
645}
646
647impl Default for NatsAuth {
648    fn default() -> Self {
649        if let (Ok(username), Ok(password)) = (
650            std::env::var(env_nats::auth::NATS_AUTH_USERNAME),
651            std::env::var(env_nats::auth::NATS_AUTH_PASSWORD),
652        ) {
653            return NatsAuth::UserPass(username, password);
654        }
655
656        if let Ok(token) = std::env::var(env_nats::auth::NATS_AUTH_TOKEN) {
657            return NatsAuth::Token(token);
658        }
659
660        if let Ok(nkey) = std::env::var(env_nats::auth::NATS_AUTH_NKEY) {
661            return NatsAuth::NKey(nkey);
662        }
663
664        if let Ok(path) = std::env::var(env_nats::auth::NATS_AUTH_CREDENTIALS_FILE) {
665            return NatsAuth::CredentialsFile(PathBuf::from(path));
666        }
667
668        NatsAuth::UserPass("user".to_string(), "user".to_string())
669    }
670}
671
672/// Extract NATS bucket and key from a nats URL of the form:
673/// `nats://host[:port]/bucket/key`
674pub fn url_to_bucket_and_key(url: &Url) -> anyhow::Result<(String, String)> {
675    let Some(mut path_segments) = url.path_segments() else {
676        anyhow::bail!("No path in NATS URL: {url}");
677    };
678    let Some(bucket) = path_segments.next() else {
679        anyhow::bail!("No bucket in NATS URL: {url}");
680    };
681    let Some(key) = path_segments.next() else {
682        anyhow::bail!("No key in NATS URL: {url}");
683    };
684    Ok((bucket.to_string(), key.to_string()))
685}
686
687/// A queue implementation using NATS JetStream
688pub struct NatsQueue {
689    /// The name of the stream to use for the queue
690    stream_name: String,
691    /// The NATS server URL
692    nats_server: String,
693    /// Timeout for dequeue operations in seconds
694    dequeue_timeout: time::Duration,
695    /// The NATS client
696    client: Option<Client>,
697    /// The subject pattern used for this queue
698    subject: String,
699    /// The subscriber for pull-based consumption
700    subscriber: Option<jetstream::consumer::PullConsumer>,
701    /// Optional consumer name for broadcast pattern (if None, uses "worker-group")
702    consumer_name: Option<String>,
703    /// Message stream for efficient message consumption
704    message_stream: Option<jetstream::consumer::pull::Stream>,
705}
706
707impl NatsQueue {
708    /// Create a new NatsQueue with the default "worker-group" consumer
709    pub fn new(stream_name: String, nats_server: String, dequeue_timeout: time::Duration) -> Self {
710        // Sanitize stream name to remove path separators (like in Python version)
711        // rupei: are we sure NATs stream name accepts '_'?
712        let sanitized_stream_name = Slug::slugify(&stream_name).to_string();
713        let subject = format!("{sanitized_stream_name}.*");
714
715        Self {
716            stream_name: sanitized_stream_name,
717            nats_server,
718            dequeue_timeout,
719            client: None,
720            subject,
721            subscriber: None,
722            consumer_name: Some("worker-group".to_string()),
723            message_stream: None,
724        }
725    }
726
727    /// Create a new NatsQueue without a consumer (publisher-only mode)
728    pub fn new_without_consumer(
729        stream_name: String,
730        nats_server: String,
731        dequeue_timeout: time::Duration,
732    ) -> Self {
733        let sanitized_stream_name = Slug::slugify(&stream_name).to_string();
734        let subject = format!("{sanitized_stream_name}.*");
735
736        Self {
737            stream_name: sanitized_stream_name,
738            nats_server,
739            dequeue_timeout,
740            client: None,
741            subject,
742            subscriber: None,
743            consumer_name: None,
744            message_stream: None,
745        }
746    }
747
748    /// Create a new NatsQueue with a specific consumer name for broadcast pattern
749    /// Each consumer with a unique name will receive all messages independently
750    pub fn new_with_consumer(
751        stream_name: String,
752        nats_server: String,
753        dequeue_timeout: time::Duration,
754        consumer_name: String,
755    ) -> Self {
756        let sanitized_stream_name = Slug::slugify(&stream_name).to_string();
757        let subject = format!("{sanitized_stream_name}.*");
758
759        Self {
760            stream_name: sanitized_stream_name,
761            nats_server,
762            dequeue_timeout,
763            client: None,
764            subject,
765            subscriber: None,
766            consumer_name: Some(consumer_name),
767            message_stream: None,
768        }
769    }
770
771    /// Connect to the NATS server and set up the stream and consumer
772    pub async fn connect(&mut self) -> Result<()> {
773        self.connect_with_reset(false).await
774    }
775
776    /// Connect to the NATS server and set up the stream and consumer, optionally resetting the stream
777    pub async fn connect_with_reset(&mut self, reset_stream: bool) -> Result<()> {
778        if self.client.is_none() {
779            // Create a new client
780            let client_options = Client::builder().server(self.nats_server.clone()).build()?;
781
782            let client = client_options.connect().await?;
783
784            // messages older than a hour in the stream will be automatically purged
785            let max_age = std::env::var(env_nats::stream::DYN_NATS_STREAM_MAX_AGE)
786                .ok()
787                .and_then(|s| s.parse::<u64>().ok())
788                .map(time::Duration::from_secs)
789                .unwrap_or_else(|| time::Duration::from_secs(60 * 60));
790
791            let stream_config = jetstream::stream::Config {
792                name: self.stream_name.clone(),
793                subjects: vec![self.subject.clone()],
794                max_age,
795                ..Default::default()
796            };
797
798            // Get or create the stream
799            let stream = client
800                .jetstream()
801                .get_or_create_stream(stream_config)
802                .await?;
803
804            log::debug!("Stream {} is ready", self.stream_name);
805
806            // If reset_stream is true, purge all messages from the stream
807            if reset_stream {
808                match stream.purge().await {
809                    Ok(purge_info) => {
810                        log::info!(
811                            "Successfully purged {} messages from NATS stream {}",
812                            purge_info.purged,
813                            self.stream_name
814                        );
815                    }
816                    Err(e) => {
817                        log::warn!("Failed to purge NATS stream '{}': {e}", self.stream_name);
818                    }
819                }
820            }
821
822            // Create persistent subscriber only if consumer_name is set
823            if let Some(ref consumer_name) = self.consumer_name {
824                let consumer_config = jetstream::consumer::pull::Config {
825                    durable_name: Some(consumer_name.clone()),
826                    inactive_threshold: std::time::Duration::from_secs(300), // 5 minutes
827                    ..Default::default()
828                };
829
830                let subscriber = stream.create_consumer(consumer_config).await?;
831
832                // Create the message stream for efficient consumption
833                let message_stream = subscriber.messages().await?;
834
835                self.subscriber = Some(subscriber);
836                self.message_stream = Some(message_stream);
837            }
838
839            self.client = Some(client);
840        }
841
842        Ok(())
843    }
844
845    /// Ensure we have an active connection
846    pub async fn ensure_connection(&mut self) -> Result<()> {
847        if self.client.is_none() {
848            self.connect().await?;
849        }
850        Ok(())
851    }
852
853    /// Close the connection when done
854    pub async fn close(&mut self) -> Result<()> {
855        self.message_stream = None;
856        self.subscriber = None;
857        self.client = None;
858        Ok(())
859    }
860
861    /// Shutdown the consumer by deleting it from the stream and closing the connection
862    /// This permanently removes the consumer from the server
863    ///
864    /// If `consumer_name` is provided, that specific consumer will be deleted instead of the
865    /// current consumer. This allows deletion of other consumers on the same stream.
866    pub async fn shutdown(&mut self, consumer_name: Option<String>) -> Result<()> {
867        // Determine which consumer to delete
868        let target_consumer = consumer_name.as_ref().or(self.consumer_name.as_ref());
869
870        // Warn if deleting our own consumer via explicit parameter
871        if let Some(ref passed_name) = consumer_name
872            && self.consumer_name.as_ref() == Some(passed_name)
873        {
874            log::warn!(
875                "Deleting our own consumer '{}' via explicit consumer_name parameter. \
876                Consider calling shutdown without arguments instead.",
877                passed_name
878            );
879        }
880
881        if let (Some(client), Some(consumer_to_delete)) = (&self.client, target_consumer) {
882            // Get the stream and delete the consumer
883            let stream = client.jetstream().get_stream(&self.stream_name).await?;
884            stream
885                .delete_consumer(consumer_to_delete)
886                .await
887                .map_err(|e| {
888                    anyhow::anyhow!("Failed to delete consumer {}: {}", consumer_to_delete, e)
889                })?;
890            log::debug!(
891                "Deleted consumer {} from stream {}",
892                consumer_to_delete,
893                self.stream_name
894            );
895        } else {
896            log::debug!(
897                "Cannot shutdown consumer: client or target consumer is None (client: {:?}, target_consumer: {:?})",
898                self.client.is_some(),
899                target_consumer.is_some()
900            );
901        }
902
903        // Only close the connection if we deleted our own consumer
904        if consumer_name.is_none() {
905            self.close().await
906        } else {
907            Ok(())
908        }
909    }
910
911    /// Count the number of consumers for the stream
912    pub async fn count_consumers(&mut self) -> Result<usize> {
913        self.ensure_connection().await?;
914
915        if let Some(client) = &self.client {
916            let mut stream = client.jetstream().get_stream(&self.stream_name).await?;
917            let info = stream.info().await?;
918            Ok(info.state.consumer_count)
919        } else {
920            Err(anyhow::anyhow!("Client not connected"))
921        }
922    }
923
924    /// List all consumer names for the stream
925    pub async fn list_consumers(&mut self) -> Result<Vec<String>> {
926        self.ensure_connection().await?;
927
928        if let Some(client) = &self.client {
929            client.list_consumers(&self.stream_name).await
930        } else {
931            Err(anyhow::anyhow!("Client not connected"))
932        }
933    }
934
935    /// Enqueue a task using the provided data
936    pub async fn enqueue_task(&mut self, task_data: Bytes) -> Result<()> {
937        self.ensure_connection().await?;
938
939        if let Some(client) = &self.client {
940            let subject = format!("{}.queue", self.stream_name);
941            client.jetstream().publish(subject, task_data).await?;
942            Ok(())
943        } else {
944            Err(anyhow::anyhow!("Client not connected"))
945        }
946    }
947
948    /// Dequeue and return a task as raw bytes
949    pub async fn dequeue_task(&mut self, timeout: Option<time::Duration>) -> Result<Option<Bytes>> {
950        self.ensure_connection().await?;
951
952        let Some(ref mut stream) = self.message_stream else {
953            return Err(anyhow::anyhow!("Message stream not initialized"));
954        };
955
956        let timeout_duration = timeout.unwrap_or(self.dequeue_timeout);
957
958        // Try to get next message from the stream with timeout
959        let message = tokio::time::timeout(timeout_duration, stream.next()).await;
960
961        match message {
962            Ok(Some(Ok(msg))) => {
963                msg.ack()
964                    .await
965                    .map_err(|e| anyhow::anyhow!("Failed to ack message: {}", e))?;
966                Ok(Some(msg.payload.clone()))
967            }
968
969            Ok(Some(Err(e))) => Err(anyhow::anyhow!("Failed to get message from stream: {}", e)),
970
971            Ok(None) => Err(anyhow::anyhow!("Message stream ended unexpectedly")),
972
973            // Timeout - no messages available
974            Err(_) => Ok(None),
975        }
976    }
977
978    /// Get the number of messages currently in the queue
979    pub async fn get_queue_size(&mut self) -> Result<u64> {
980        self.ensure_connection().await?;
981
982        if let Some(client) = &self.client {
983            // Get consumer info to get pending messages count
984            let stream = client.jetstream().get_stream(&self.stream_name).await?;
985            let consumer_name = self
986                .consumer_name
987                .clone()
988                .unwrap_or_else(|| "worker-group".to_string());
989            let mut consumer: jetstream::consumer::PullConsumer = stream
990                .get_consumer(&consumer_name)
991                .await
992                .map_err(|e| anyhow::anyhow!("Failed to get consumer: {}", e))?;
993            let info = consumer.info().await?;
994
995            Ok(info.num_pending)
996        } else {
997            Err(anyhow::anyhow!("Client not connected"))
998        }
999    }
1000
1001    /// Get the total number of messages currently in the stream
1002    pub async fn get_stream_messages(&mut self) -> Result<u64> {
1003        self.ensure_connection().await?;
1004
1005        if let Some(client) = &self.client {
1006            let mut stream = client.jetstream().get_stream(&self.stream_name).await?;
1007            let info = stream.info().await?;
1008            Ok(info.state.messages)
1009        } else {
1010            Err(anyhow::anyhow!("Client not connected"))
1011        }
1012    }
1013
1014    /// Purge messages from the stream up to (but not including) the specified sequence number
1015    /// This permanently removes messages and affects all consumers of the stream
1016    pub async fn purge_up_to_sequence(&self, sequence: u64) -> Result<()> {
1017        if let Some(client) = &self.client {
1018            let stream = client.jetstream().get_stream(&self.stream_name).await?;
1019
1020            // NOTE: this purge excludes the sequence itself
1021            // https://docs.rs/nats/latest/nats/jetstream/struct.PurgeRequest.html
1022            stream.purge().sequence(sequence).await.map_err(|e| {
1023                anyhow::anyhow!("Failed to purge stream up to sequence {}: {}", sequence, e)
1024            })?;
1025
1026            log::debug!(
1027                "Purged stream {} up to sequence {}",
1028                self.stream_name,
1029                sequence
1030            );
1031            Ok(())
1032        } else {
1033            Err(anyhow::anyhow!("Client not connected"))
1034        }
1035    }
1036
1037    /// Purge messages from the stream up to the minimum acknowledged sequence across all consumers
1038    /// This finds the lowest acknowledged sequence number across all consumers and purges up to that point
1039    pub async fn purge_acknowledged(&mut self) -> Result<()> {
1040        self.ensure_connection().await?;
1041
1042        let Some(client) = &self.client else {
1043            return Err(anyhow::anyhow!("Client not connected"));
1044        };
1045
1046        let stream = client.jetstream().get_stream(&self.stream_name).await?;
1047
1048        // Get all consumer names for the stream
1049        let consumer_names: Vec<String> = stream
1050            .consumer_names()
1051            .try_collect()
1052            .await
1053            .map_err(|e| anyhow::anyhow!("Failed to list consumers: {}", e))?;
1054
1055        if consumer_names.is_empty() {
1056            log::debug!("No consumers found for stream {}", self.stream_name);
1057            return Ok(());
1058        }
1059
1060        // Find the minimum acknowledged sequence across all consumers
1061        let mut min_ack_sequence = u64::MAX;
1062
1063        for consumer_name in &consumer_names {
1064            let mut consumer: jetstream::consumer::PullConsumer = stream
1065                .get_consumer(consumer_name)
1066                .await
1067                .map_err(|e| anyhow::anyhow!("Failed to get consumer {}: {}", consumer_name, e))?;
1068
1069            let info = consumer.info().await.map_err(|e| {
1070                anyhow::anyhow!("Failed to get consumer info for {}: {}", consumer_name, e)
1071            })?;
1072
1073            // The ack_floor contains the stream sequence of the highest contiguously acknowledged message
1074            // If stream_sequence is 0, it means no messages have been acknowledged yet
1075            if info.ack_floor.stream_sequence > 0 {
1076                min_ack_sequence = min_ack_sequence.min(info.ack_floor.stream_sequence);
1077                log::debug!(
1078                    "Consumer {} has ack_floor at sequence {}",
1079                    consumer_name,
1080                    info.ack_floor.stream_sequence
1081                );
1082            }
1083        }
1084
1085        // Only purge if we found a valid minimum acknowledged sequence
1086        if min_ack_sequence < u64::MAX && min_ack_sequence > 0 {
1087            // Purge up to (but not including) the minimum acknowledged sequence + 1
1088            // We add 1 because we want to include the minimum acknowledged message in the purge
1089            let purge_sequence = min_ack_sequence + 1;
1090
1091            self.purge_up_to_sequence(purge_sequence).await?;
1092
1093            log::debug!(
1094                "Purged stream {} up to acknowledged sequence {} (purged up to sequence {})",
1095                self.stream_name,
1096                min_ack_sequence,
1097                purge_sequence
1098            );
1099        } else {
1100            log::debug!(
1101                "No messages to purge for stream {} (min_ack_sequence: {})",
1102                self.stream_name,
1103                min_ack_sequence
1104            );
1105        }
1106
1107        Ok(())
1108    }
1109}
1110
1111impl NatsQueue {
1112    pub fn event_subject(&self) -> String {
1113        self.stream_name.clone()
1114    }
1115
1116    pub async fn publish_event(
1117        &self,
1118        event_name: impl AsRef<str> + Send + Sync,
1119        event: &(impl Serialize + Send + Sync),
1120    ) -> Result<()> {
1121        let bytes = serde_json::to_vec(event)?;
1122        self.publish_event_bytes(event_name, bytes).await
1123    }
1124
1125    pub async fn publish_event_bytes(
1126        &self,
1127        event_name: impl AsRef<str> + Send + Sync,
1128        bytes: Vec<u8>,
1129    ) -> Result<()> {
1130        let subject = format!("{}.{}", self.event_subject(), event_name.as_ref());
1131
1132        // Note: enqueue_task requires &mut self, but EventPublisher requires &self
1133        // We need to ensure the client is connected and use it directly
1134        if let Some(client) = &self.client {
1135            client.jetstream().publish(subject, bytes.into()).await?;
1136            Ok(())
1137        } else {
1138            Err(anyhow::anyhow!("Client not connected"))
1139        }
1140    }
1141}
1142
1143/// The NATS subject / inbox to talk to an instance on.
1144/// TODO: Do we need to sanitize the names?
1145pub fn instance_subject(endpoint_id: &EndpointId, instance_id: u64) -> String {
1146    format!(
1147        "{}_{}.{}-{:x}",
1148        endpoint_id.namespace, endpoint_id.component, endpoint_id.name, instance_id,
1149    )
1150}
1151
1152#[cfg(test)]
1153mod tests {
1154
1155    use super::*;
1156    use figment::Jail;
1157    use serde::{Deserialize, Serialize};
1158
1159    #[test]
1160    fn parses_startup_connect_timeout() {
1161        for (value, expected) in [
1162            (None, DEFAULT_STARTUP_CONNECT_TIMEOUT),
1163            (Some("45"), Duration::from_secs(45)),
1164            (Some("0"), DEFAULT_STARTUP_CONNECT_TIMEOUT),
1165            (Some("invalid"), DEFAULT_STARTUP_CONNECT_TIMEOUT),
1166        ] {
1167            assert_eq!(startup_connect_timeout_from_value(value), expected);
1168        }
1169    }
1170
1171    #[rstest::rstest]
1172    #[case::connection_refused(false, 1200, ConnectErrorKind::Io)]
1173    #[case::stalled_handshake(true, 6500, ConnectErrorKind::TimedOut)]
1174    #[tokio::test]
1175    async fn startup_connection_waits_for_deadline(
1176        #[case] stalled_handshake: bool,
1177        #[case] timeout_ms: u64,
1178        #[case] expected_kind: ConnectErrorKind,
1179    ) {
1180        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1181        let server = format!("nats://{}", listener.local_addr().unwrap());
1182        let (accepted, mut connections) = tokio::sync::mpsc::unbounded_channel();
1183        let accept_task = if stalled_handshake {
1184            Some(tokio::spawn(async move {
1185                loop {
1186                    let (stream, _) = listener.accept().await.unwrap();
1187                    if accepted.send(stream).is_err() {
1188                        break;
1189                    }
1190                }
1191            }))
1192        } else {
1193            drop(listener);
1194            None
1195        };
1196
1197        let timeout = Duration::from_millis(timeout_ms);
1198        let started = time::Instant::now();
1199        let error = time::timeout(
1200            timeout + Duration::from_secs(2),
1201            connect_with_startup_retry(
1202                ConnectOptions::new().connection_timeout(Duration::from_secs(60)),
1203                server,
1204                timeout,
1205                CancellationToken::new(),
1206            ),
1207        )
1208        .await
1209        .expect("startup exceeded its deadline")
1210        .unwrap_err();
1211        if let Some(task) = accept_task {
1212            task.abort();
1213            assert!(connections.try_recv().is_ok());
1214            assert!(connections.try_recv().is_ok());
1215            assert!(connections.try_recv().is_err());
1216        }
1217        assert!(
1218            error.to_string().contains(&format!(
1219                "timed out after {} seconds (2 attempts)",
1220                timeout.as_secs_f64()
1221            )),
1222            "{error:#}"
1223        );
1224        assert!(started.elapsed() >= timeout);
1225        assert_eq!(
1226            error.downcast_ref::<ConnectError>().unwrap().kind(),
1227            expected_kind
1228        );
1229    }
1230
1231    #[rstest::rstest]
1232    #[case::attempt(true)]
1233    #[case::backoff(false)]
1234    #[tokio::test]
1235    async fn cancellation_interrupts_startup(#[case] during_attempt: bool) {
1236        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1237        let options = ClientOptions::builder()
1238            .server(format!("nats://{}", listener.local_addr().unwrap()))
1239            .build()
1240            .unwrap();
1241        let listener = during_attempt.then_some(listener);
1242        let token = CancellationToken::new();
1243        let connection = tokio::spawn(options.connect_with_cancellation(token.clone()));
1244        if let Some(listener) = listener {
1245            let (_stream, _) = time::timeout(Duration::from_secs(2), listener.accept())
1246                .await
1247                .unwrap()
1248                .unwrap();
1249            token.cancel();
1250        } else {
1251            time::sleep(Duration::from_millis(100)).await;
1252            assert!(!connection.is_finished());
1253            token.cancel();
1254        }
1255        let result = time::timeout(Duration::from_millis(500), connection)
1256            .await
1257            .unwrap()
1258            .unwrap();
1259        match result {
1260            Err(error) => assert_eq!(error.to_string(), "NATS startup connection cancelled"),
1261            Ok(_) => panic!("cancelled connection unexpectedly succeeded"),
1262        }
1263    }
1264
1265    #[tokio::test]
1266    async fn authorization_failure_does_not_retry() {
1267        use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
1268
1269        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1270        let server = format!("nats://{}", listener.local_addr().unwrap());
1271        let accept_task = tokio::spawn(async move {
1272            let (mut stream, _) = listener.accept().await.unwrap();
1273            stream.write_all(b"INFO {}\r\n").await.unwrap();
1274            let mut stream = BufReader::new(stream);
1275            let mut line = String::new();
1276            stream.read_line(&mut line).await.unwrap();
1277            assert!(line.starts_with("CONNECT "));
1278            stream
1279                .get_mut()
1280                .write_all(b"-ERR 'Authorization Violation'\r\n")
1281                .await
1282                .unwrap();
1283        });
1284        let error = time::timeout(
1285            Duration::from_millis(500),
1286            connect_with_startup_retry(
1287                ConnectOptions::new(),
1288                server,
1289                DEFAULT_STARTUP_CONNECT_TIMEOUT,
1290                CancellationToken::new(),
1291            ),
1292        )
1293        .await
1294        .unwrap()
1295        .unwrap_err();
1296        accept_task.await.unwrap();
1297        assert_eq!(
1298            error.downcast_ref::<ConnectError>().unwrap().kind(),
1299            ConnectErrorKind::AuthorizationViolation
1300        );
1301    }
1302
1303    #[tokio::test]
1304    async fn startup_connection_recovers_after_failed_handshake() {
1305        use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
1306
1307        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1308        let options = ClientOptions::builder()
1309            .server(format!("nats://{}", listener.local_addr().unwrap()))
1310            .startup_connect_timeout(Duration::from_secs(3))
1311            .build()
1312            .unwrap();
1313        let server = tokio::spawn(async move {
1314            let (first, _) = listener.accept().await.unwrap();
1315            drop(first);
1316
1317            let (mut stream, _) = listener.accept().await.unwrap();
1318            stream.write_all(b"INFO {}\r\n").await.unwrap();
1319            let mut stream = BufReader::new(stream);
1320            let mut line = String::new();
1321            stream.read_line(&mut line).await.unwrap();
1322            assert!(line.starts_with("CONNECT "));
1323            line.clear();
1324            stream.read_line(&mut line).await.unwrap();
1325            assert_eq!(line, "PING\r\n");
1326            stream.get_mut().write_all(b"PONG\r\n").await.unwrap();
1327            stream
1328        });
1329
1330        let client = time::timeout(Duration::from_secs(4), options.connect())
1331            .await
1332            .expect("startup did not recover")
1333            .expect("startup connection failed");
1334        let _stream = server.await.unwrap();
1335        assert_eq!(client.client.connection_state(), State::Connected);
1336    }
1337
1338    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1339    struct TestData {
1340        id: u32,
1341        name: String,
1342        values: Vec<f64>,
1343    }
1344
1345    #[test]
1346    #[allow(clippy::result_large_err)]
1347    fn test_client_options_builder() {
1348        Jail::expect_with(|_jail| {
1349            let opts = ClientOptions::builder().build();
1350            assert!(opts.is_ok());
1351            Ok(())
1352        });
1353
1354        Jail::expect_with(|jail| {
1355            jail.set_env(env_nats::NATS_SERVER, "nats://localhost:5222");
1356            jail.set_env(env_nats::auth::NATS_AUTH_USERNAME, "user");
1357            jail.set_env(env_nats::auth::NATS_AUTH_PASSWORD, "pass");
1358
1359            let opts = ClientOptions::builder().build();
1360            assert!(opts.is_ok());
1361            let opts = opts.unwrap();
1362
1363            assert_eq!(opts.server, "nats://localhost:5222");
1364            assert_eq!(
1365                opts.auth,
1366                NatsAuth::UserPass("user".to_string(), "pass".to_string())
1367            );
1368
1369            Ok(())
1370        });
1371
1372        Jail::expect_with(|jail| {
1373            jail.set_env(env_nats::NATS_SERVER, "nats://localhost:5222");
1374            jail.set_env(env_nats::auth::NATS_AUTH_USERNAME, "user");
1375            jail.set_env(env_nats::auth::NATS_AUTH_PASSWORD, "pass");
1376
1377            let opts = ClientOptions::builder()
1378                .server("nats://localhost:6222")
1379                .auth(NatsAuth::Token("token".to_string()))
1380                .build();
1381            assert!(opts.is_ok());
1382            let opts = opts.unwrap();
1383
1384            assert_eq!(opts.server, "nats://localhost:6222");
1385            assert_eq!(opts.auth, NatsAuth::Token("token".to_string()));
1386
1387            Ok(())
1388        });
1389    }
1390
1391    #[test]
1392    #[allow(clippy::result_large_err)]
1393    fn test_client_options_tls_url_validation() {
1394        // tls:// is accepted by the validator
1395        assert!(validate_nats_server("tls://nats:4222").is_ok());
1396        assert!(validate_nats_server("nats://nats:4222").is_ok());
1397        assert!(validate_nats_server("tcp://nats:4222").is_err());
1398        assert!(validate_nats_server("http://nats:4222").is_err());
1399
1400        // tls:// URL is preserved in options
1401        Jail::expect_with(|jail| {
1402            jail.set_env(env_nats::NATS_SERVER, "tls://nats:4222");
1403            let opts = ClientOptions::builder().build().unwrap();
1404            assert_eq!(opts.server, "tls://nats:4222");
1405            Ok(())
1406        });
1407    }
1408
1409    #[test]
1410    #[allow(clippy::result_large_err)]
1411    fn test_client_options_tls_ca_from_env() {
1412        Jail::expect_with(|jail| {
1413            jail.set_env(env_nats::NATS_SERVER, "tls://nats:4222");
1414            jail.set_env(env_nats::tls::NATS_TLS_CA_CERT_PATH, "/etc/certs/ca.pem");
1415            let opts = ClientOptions::builder().build().unwrap();
1416            assert_eq!(
1417                opts.tls_ca_cert_path,
1418                Some(PathBuf::from("/etc/certs/ca.pem"))
1419            );
1420            assert!(!opts.tls_insecure);
1421            Ok(())
1422        });
1423    }
1424
1425    #[test]
1426    #[allow(clippy::result_large_err)]
1427    fn test_client_options_tls_insecure_from_env() {
1428        Jail::expect_with(|jail| {
1429            jail.set_env(env_nats::NATS_SERVER, "tls://nats:4222");
1430            jail.set_env(env_nats::tls::NATS_TLS_INSECURE, "1");
1431            let opts = ClientOptions::builder().build().unwrap();
1432            assert!(opts.tls_insecure);
1433            assert!(opts.tls_ca_cert_path.is_none());
1434            Ok(())
1435        });
1436    }
1437
1438    #[test]
1439    #[allow(clippy::result_large_err)]
1440    fn test_client_options_no_tls_by_default() {
1441        Jail::expect_with(|_jail| {
1442            let opts = ClientOptions::builder().build().unwrap();
1443            assert!(opts.tls_ca_cert_path.is_none());
1444            assert!(!opts.tls_insecure);
1445            Ok(())
1446        });
1447    }
1448
1449    #[tokio::test]
1450    async fn test_connect_rejects_custom_tls_with_nats_url() {
1451        // Client is not Debug, so match rather than use unwrap_err/expect_err.
1452        fn assert_scheme_error(result: Result<Client>, case: &str) {
1453            match result {
1454                Ok(_) => panic!("{case}: expected an error, got a connection"),
1455                Err(e) => assert!(
1456                    e.to_string().contains("tls://"),
1457                    "{case}: unexpected error: {e}"
1458                ),
1459            }
1460        }
1461
1462        // CA cert set but server is nats:// (not tls://) → rejected before connecting.
1463        let opts = ClientOptions::builder()
1464            .server("nats://localhost:4222".to_string())
1465            .tls_ca_cert_path(Some(PathBuf::from("/etc/certs/ca.pem")))
1466            .build()
1467            .unwrap();
1468        assert_scheme_error(opts.connect().await, "nats:// + CA cert");
1469
1470        // Insecure mode set but server is nats:// → also rejected.
1471        let opts = ClientOptions::builder()
1472            .server("nats://localhost:4222".to_string())
1473            .tls_insecure(true)
1474            .build()
1475            .unwrap();
1476        assert_scheme_error(opts.connect().await, "nats:// + insecure");
1477    }
1478
1479    #[test]
1480    #[allow(clippy::result_large_err)]
1481    fn test_nats_mtls_client_cert_from_env() {
1482        Jail::expect_with(|jail| {
1483            jail.set_env(env_nats::NATS_SERVER, "tls://localhost:4222");
1484            jail.set_env(env_nats::tls::NATS_TLS_CA_CERT_PATH, "/etc/certs/ca.pem");
1485            jail.set_env(
1486                env_nats::tls::NATS_TLS_CLIENT_CERT_PATH,
1487                "/etc/certs/client.pem",
1488            );
1489            jail.set_env(
1490                env_nats::tls::NATS_TLS_CLIENT_KEY_PATH,
1491                "/etc/certs/client-key.pem",
1492            );
1493            let opts = ClientOptions::builder().build().unwrap();
1494            assert_eq!(
1495                opts.tls_client_cert_path,
1496                Some(PathBuf::from("/etc/certs/client.pem"))
1497            );
1498            assert_eq!(
1499                opts.tls_client_key_path,
1500                Some(PathBuf::from("/etc/certs/client-key.pem"))
1501            );
1502            Ok(())
1503        });
1504    }
1505
1506    #[tokio::test]
1507    async fn test_nats_mtls_client_cert_requires_ca() {
1508        // Client cert/key set, tls:// URL, but no CA and not insecure → rejected.
1509        let opts = ClientOptions::builder()
1510            .server("tls://localhost:4222".to_string())
1511            .tls_client_cert_path(Some(PathBuf::from("/tmp/client.pem")))
1512            .tls_client_key_path(Some(PathBuf::from("/tmp/client-key.pem")))
1513            .build()
1514            .unwrap();
1515        match opts.connect().await {
1516            Err(e) => assert!(
1517                e.to_string().contains("requires"),
1518                "expected CA-requirement error, got: {e}"
1519            ),
1520            Ok(_) => panic!("expected error when client cert set without CA"),
1521        }
1522    }
1523
1524    #[tokio::test]
1525    async fn test_nats_mtls_partial_client_identity_errors() {
1526        // Client cert without key → rejected before connecting.
1527        let opts = ClientOptions::builder()
1528            .server("tls://localhost:4222".to_string())
1529            .tls_ca_cert_path(Some(PathBuf::from("/tmp/ca.pem")))
1530            .tls_client_cert_path(Some(PathBuf::from("/tmp/client.pem")))
1531            .build()
1532            .unwrap();
1533        match opts.connect().await {
1534            Err(e) => assert!(
1535                e.to_string().contains("must be set together"),
1536                "expected both-or-neither error, got: {e}"
1537            ),
1538            Ok(_) => panic!("expected error when client cert set without key"),
1539        }
1540    }
1541
1542    // Integration test for object store data operations using bincode
1543    #[tokio::test]
1544    #[ignore] // Requires NATS server to be running
1545    async fn test_object_store_data_operations() {
1546        // Create test data
1547        let test_data = TestData {
1548            id: 42,
1549            name: "test_item".to_string(),
1550            values: vec![1.0, 2.5, 3.7, 4.2],
1551        };
1552
1553        // Set up client
1554        let client_options = ClientOptions::builder()
1555            .server("nats://localhost:4222")
1556            .build()
1557            .expect("Failed to build client options");
1558
1559        let client = client_options
1560            .connect()
1561            .await
1562            .expect("Failed to connect to NATS");
1563
1564        // Test URL (using .bin extension to indicate binary format)
1565        let url =
1566            Url::parse("nats://localhost/test-bucket/test-data.bin").expect("Failed to parse URL");
1567
1568        // Upload the data
1569        client
1570            .object_store_upload_data(&test_data, &url)
1571            .await
1572            .expect("Failed to upload data");
1573
1574        // Download the data
1575        let downloaded_data: TestData = client
1576            .object_store_download_data(&url)
1577            .await
1578            .expect("Failed to download data");
1579
1580        // Verify the data matches
1581        assert_eq!(test_data, downloaded_data);
1582
1583        // Clean up
1584        client
1585            .object_store_delete_bucket("test-bucket")
1586            .await
1587            .expect("Failed to delete bucket");
1588    }
1589
1590    // Integration test for broadcast pattern with purging
1591    #[tokio::test]
1592    #[ignore]
1593    async fn test_nats_queue_broadcast_with_purge() {
1594        use uuid::Uuid;
1595
1596        // Create unique stream name for this test
1597        let stream_name = format!("test-broadcast-{}", Uuid::new_v4());
1598        let nats_server = "nats://localhost:4222".to_string();
1599        let timeout = time::Duration::from_secs(0);
1600
1601        // Connect to NATS client first to delete stream if it exists
1602        let client_options = Client::builder()
1603            .server(nats_server.clone())
1604            .build()
1605            .expect("Failed to build client options");
1606
1607        let client = client_options
1608            .connect()
1609            .await
1610            .expect("Failed to connect to NATS");
1611
1612        // Delete the stream if it exists (to ensure clean start)
1613        let _ = client.jetstream().delete_stream(&stream_name).await;
1614
1615        // Create two consumers with different names for the same stream
1616        let consumer1_name = format!("consumer-{}", Uuid::new_v4());
1617        let consumer2_name = format!("consumer-{}", Uuid::new_v4());
1618
1619        let mut queue1 = NatsQueue::new_with_consumer(
1620            stream_name.clone(),
1621            nats_server.clone(),
1622            timeout,
1623            consumer1_name,
1624        );
1625
1626        // Connect queue1 first (it will create the stream)
1627        queue1.connect().await.expect("Failed to connect queue1");
1628
1629        // Send 4 messages using the EventPublisher trait
1630        let message_strings = [
1631            "message1".to_string(),
1632            "message2".to_string(),
1633            "message3".to_string(),
1634            "message4".to_string(),
1635        ];
1636
1637        // Publish messages using NatsQueue
1638        for (idx, msg) in message_strings.iter().enumerate() {
1639            queue1
1640                .publish_event("queue", msg)
1641                .await
1642                .unwrap_or_else(|_| panic!("Failed to publish message {}", idx + 1));
1643        }
1644
1645        // Convert messages to JSON-serialized Bytes for comparison
1646        let messages: Vec<Bytes> = message_strings
1647            .iter()
1648            .map(|s| Bytes::from(serde_json::to_vec(s).unwrap()))
1649            .collect();
1650
1651        // Give JetStream a moment to persist the messages
1652        tokio::time::sleep(time::Duration::from_millis(100)).await;
1653
1654        // Now create and connect queue2 and queue3 AFTER messages are published (to test persistence)
1655        let mut queue2 = NatsQueue::new_with_consumer(
1656            stream_name.clone(),
1657            nats_server.clone(),
1658            timeout,
1659            consumer2_name,
1660        );
1661
1662        // Create a third queue without consumer (publisher-only)
1663        let mut queue3 =
1664            NatsQueue::new_without_consumer(stream_name.clone(), nats_server.clone(), timeout);
1665
1666        // Connect queue2 and queue3 after messages are already published
1667        queue2.connect().await.expect("Failed to connect queue2");
1668        queue3.connect().await.expect("Failed to connect queue3");
1669
1670        // Purge the first two messages (sequence 1 and 2)
1671        // Note: JetStream sequences start at 1, and purge is exclusive of the sequence number
1672        queue1
1673            .purge_up_to_sequence(3)
1674            .await
1675            .expect("Failed to purge messages");
1676
1677        // Give JetStream a moment to process the purge
1678        tokio::time::sleep(time::Duration::from_millis(100)).await;
1679
1680        // Consumer 1 dequeues one message (message3)
1681        let msg3_consumer1 = queue1
1682            .dequeue_task(Some(time::Duration::from_millis(500)))
1683            .await
1684            .expect("Failed to dequeue from queue1");
1685        assert_eq!(
1686            msg3_consumer1,
1687            Some(messages[2].clone()),
1688            "Consumer 1 should get message3"
1689        );
1690
1691        // Give JetStream a moment to process acknowledgments
1692        tokio::time::sleep(time::Duration::from_millis(100)).await;
1693
1694        // Now run purge_acknowledged
1695        // At this point:
1696        // - Consumer 1 has ack'd message 3 (ack_floor = 3)
1697        // - Consumer 2 hasn't consumed anything yet (ack_floor = 0)
1698        // - Min ack_floor = 0, so nothing will be purged
1699        queue1
1700            .purge_acknowledged()
1701            .await
1702            .expect("Failed to purge acknowledged messages");
1703
1704        // Give JetStream a moment to process the purge
1705        tokio::time::sleep(time::Duration::from_millis(100)).await;
1706
1707        // Now collect remaining messages from both consumers
1708        let mut consumer1_remaining = Vec::new();
1709        let mut consumer2_remaining = Vec::new();
1710
1711        // Collect remaining messages from consumer 1
1712        while let Some(msg) = queue1
1713            .dequeue_task(None)
1714            .await
1715            .expect("Failed to dequeue from queue1")
1716        {
1717            consumer1_remaining.push(msg);
1718        }
1719
1720        // Collect remaining messages from consumer 2
1721        while let Some(msg) = queue2
1722            .dequeue_task(None)
1723            .await
1724            .expect("Failed to dequeue from queue2")
1725        {
1726            consumer2_remaining.push(msg);
1727        }
1728
1729        // Verify consumer 1 gets 1 remaining message (message4)
1730        assert_eq!(
1731            consumer1_remaining.len(),
1732            1,
1733            "Consumer 1 should have 1 remaining message"
1734        );
1735        assert_eq!(
1736            consumer1_remaining[0], messages[3],
1737            "Consumer 1 should get message4"
1738        );
1739
1740        // Verify consumer 2 gets 2 messages (message3 and message4)
1741        assert_eq!(
1742            consumer2_remaining.len(),
1743            2,
1744            "Consumer 2 should have 2 messages"
1745        );
1746        assert_eq!(
1747            consumer2_remaining[0], messages[2],
1748            "Consumer 2 should get message3"
1749        );
1750        assert_eq!(
1751            consumer2_remaining[1], messages[3],
1752            "Consumer 2 should get message4"
1753        );
1754
1755        // Test consumer count and shutdown behavior
1756        // First verify via consumer 1 that there are two consumers
1757        let consumer_count = queue1
1758            .count_consumers()
1759            .await
1760            .expect("Failed to count consumers");
1761        assert_eq!(consumer_count, 2, "Should have 2 consumers initially");
1762
1763        // Close consumer 1 and verify via consumer 2 that there are still two consumers
1764        queue1.close().await.expect("Failed to close queue1");
1765
1766        let consumer_count = queue2
1767            .count_consumers()
1768            .await
1769            .expect("Failed to count consumers");
1770        assert_eq!(
1771            consumer_count, 2,
1772            "Should still have 2 consumers after closing queue1"
1773        );
1774
1775        // Reconnect queue1 to be able to shutdown
1776        queue1.connect().await.expect("Failed to reconnect queue1");
1777
1778        // Shutdown consumer 1 and verify via consumer 2 that there is only one consumer left
1779        queue1
1780            .shutdown(None)
1781            .await
1782            .expect("Failed to shutdown queue1");
1783
1784        let consumer_count = queue2
1785            .count_consumers()
1786            .await
1787            .expect("Failed to count consumers");
1788        assert_eq!(
1789            consumer_count, 1,
1790            "Should have only 1 consumer after shutting down queue1"
1791        );
1792
1793        // Clean up by deleting the stream
1794        client
1795            .jetstream()
1796            .delete_stream(&stream_name)
1797            .await
1798            .expect("Failed to delete test stream");
1799    }
1800}