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.
19use crate::metrics::MetricsHierarchy;
20use crate::protocols::EndpointId;
21
22use anyhow::Result;
23use async_nats::connection::State;
24use async_nats::{Subscriber, client, jetstream};
25use async_trait::async_trait;
26use bytes::Bytes;
27use derive_builder::Builder;
28use futures::{StreamExt, TryStreamExt};
29use prometheus::{Counter, Gauge, Histogram, HistogramOpts, IntCounter, IntGauge, Opts, Registry};
30use serde::de::DeserializeOwned;
31use serde::{Deserialize, Serialize};
32use std::path::{Path, PathBuf};
33use std::sync::atomic::Ordering;
34use tokio::fs::File as TokioFile;
35use tokio::io::AsyncRead;
36use tokio::time;
37use url::Url;
38use validator::{Validate, ValidationError};
39
40use crate::config::environment_names::nats as env_nats;
41pub use crate::slug::Slug;
42use tracing as log;
43
44use super::utils::build_in_runtime;
45
46pub const URL_PREFIX: &str = "nats://";
47
48#[derive(Clone)]
49pub struct Client {
50    client: client::Client,
51    js_ctx: jetstream::Context,
52}
53
54impl Client {
55    /// Create a NATS [`ClientOptionsBuilder`].
56    pub fn builder() -> ClientOptionsBuilder {
57        ClientOptionsBuilder::default()
58    }
59
60    /// Returns a reference to the underlying [`async_nats::client::Client`] instance
61    pub fn client(&self) -> &client::Client {
62        &self.client
63    }
64
65    /// Returns a reference to the underlying [`async_nats::jetstream::Context`] instance
66    pub fn jetstream(&self) -> &jetstream::Context {
67        &self.js_ctx
68    }
69
70    /// host:port of NATS
71    pub fn addr(&self) -> String {
72        let info = self.client.server_info();
73        format!("{}:{}", info.host, info.port)
74    }
75
76    /// fetch the list of streams
77    pub async fn list_streams(&self) -> Result<Vec<String>> {
78        let names = self.js_ctx.stream_names();
79        let stream_names: Vec<String> = names.try_collect().await?;
80        Ok(stream_names)
81    }
82
83    /// fetch the list of consumers for a given stream
84    pub async fn list_consumers(&self, stream_name: &str) -> Result<Vec<String>> {
85        let stream = self.js_ctx.get_stream(stream_name).await?;
86        let consumers: Vec<String> = stream.consumer_names().try_collect().await?;
87        Ok(consumers)
88    }
89
90    pub async fn stream_info(&self, stream_name: &str) -> Result<jetstream::stream::State> {
91        let mut stream = self.js_ctx.get_stream(stream_name).await?;
92        let info = stream.info().await?;
93        Ok(info.state.clone())
94    }
95
96    pub async fn get_stream(&self, name: &str) -> Result<jetstream::stream::Stream> {
97        let stream = self.js_ctx.get_stream(name).await?;
98        Ok(stream)
99    }
100
101    /// Issues a broadcast request for all services with the provided `service_name` to report their
102    /// current stats. Each service will only respond once. The service may have customized the reply
103    /// so the caller should select which endpoint and what concrete data model should be used to
104    /// extract the details.
105    ///
106    /// Note: Because each endpoint will only reply once, the caller must drop the subscription after
107    /// some time or it will await forever.
108    pub async fn scrape_service(&self, service_name: &str) -> Result<Subscriber> {
109        let subject = format!("$SRV.STATS.{}", service_name);
110        let reply_subject = format!("_INBOX.{}", nuid::next());
111        let subscription = self.client.subscribe(reply_subject.clone()).await?;
112
113        // Publish the request with the reply-to subject
114        self.client
115            .publish_with_reply(subject, reply_subject, "".into())
116            .await?;
117
118        Ok(subscription)
119    }
120
121    /// Helper method to get or optionally create an object store bucket
122    ///
123    /// # Arguments
124    /// * `bucket_name` - The name of the bucket to retrieve
125    /// * `create_if_not_found` - If true, creates the bucket when it doesn't exist
126    ///
127    /// # Returns
128    /// The object store bucket or an error
129    async fn get_or_create_bucket(
130        &self,
131        bucket_name: &str,
132        create_if_not_found: bool,
133    ) -> anyhow::Result<jetstream::object_store::ObjectStore> {
134        let context = self.jetstream();
135
136        match context.get_object_store(bucket_name).await {
137            Ok(bucket) => Ok(bucket),
138            Err(err) if err.to_string().contains("stream not found") => {
139                // err.source() is GetStreamError, which has a kind() which
140                // is GetStreamErrorKind::JetStream which wraps a jetstream::Error
141                // which has code 404. Phew. So yeah check the string for now.
142
143                if create_if_not_found {
144                    tracing::debug!("Creating NATS bucket {bucket_name}");
145                    context
146                        .create_object_store(jetstream::object_store::Config {
147                            bucket: bucket_name.to_string(),
148                            ..Default::default()
149                        })
150                        .await
151                        .map_err(|e| anyhow::anyhow!("Failed creating bucket / object store: {e}"))
152                } else {
153                    anyhow::bail!(
154                        "NATS get_object_store bucket does not exist: {bucket_name}. {err}."
155                    );
156                }
157            }
158            Err(err) => {
159                anyhow::bail!("NATS get_object_store error: {err}");
160            }
161        }
162    }
163
164    /// Upload file to NATS at this URL
165    pub async fn object_store_upload(&self, filepath: &Path, nats_url: &Url) -> anyhow::Result<()> {
166        let mut disk_file = TokioFile::open(filepath).await?;
167
168        let (bucket_name, key) = url_to_bucket_and_key(nats_url)?;
169        let bucket = self.get_or_create_bucket(&bucket_name, true).await?;
170
171        let key_meta = async_nats::jetstream::object_store::ObjectMetadata {
172            name: key.to_string(),
173            ..Default::default()
174        };
175        bucket.put(key_meta, &mut disk_file).await.map_err(|e| {
176            anyhow::anyhow!("Failed uploading to bucket / object store {bucket_name}/{key}: {e}")
177        })?;
178
179        Ok(())
180    }
181
182    /// Download file from NATS at this URL
183    pub async fn object_store_download(
184        &self,
185        nats_url: &Url,
186        filepath: &Path,
187    ) -> anyhow::Result<()> {
188        let mut disk_file = TokioFile::create(filepath).await?;
189
190        let (bucket_name, key) = url_to_bucket_and_key(nats_url)?;
191        let bucket = self.get_or_create_bucket(&bucket_name, false).await?;
192
193        let mut obj_reader = bucket.get(&key).await.map_err(|e| {
194            anyhow::anyhow!(
195                "Failed downloading from bucket / object store {bucket_name}/{key}: {e}"
196            )
197        })?;
198        let _bytes_copied = tokio::io::copy(&mut obj_reader, &mut disk_file).await?;
199
200        Ok(())
201    }
202
203    /// Delete a bucket and all it's contents from the NATS object store
204    pub async fn object_store_delete_bucket(&self, bucket_name: &str) -> anyhow::Result<()> {
205        let context = self.jetstream();
206        match context.delete_object_store(&bucket_name).await {
207            Ok(_) => Ok(()),
208            Err(err) if err.to_string().contains("stream not found") => {
209                tracing::trace!(bucket_name, "NATS bucket already gone");
210                Ok(())
211            }
212            Err(err) => Err(anyhow::anyhow!("NATS get_object_store error: {err}")),
213        }
214    }
215
216    /// Upload a serializable struct to NATS object store using bincode
217    pub async fn object_store_upload_data<T>(&self, data: &T, nats_url: &Url) -> anyhow::Result<()>
218    where
219        T: Serialize,
220    {
221        // Serialize the data using bincode (more efficient binary format)
222        let binary_data = bincode::serialize(data)
223            .map_err(|e| anyhow::anyhow!("Failed to serialize data with bincode: {e}"))?;
224
225        let (bucket_name, key) = url_to_bucket_and_key(nats_url)?;
226        let bucket = self.get_or_create_bucket(&bucket_name, true).await?;
227
228        let key_meta = async_nats::jetstream::object_store::ObjectMetadata {
229            name: key.to_string(),
230            ..Default::default()
231        };
232
233        // Upload the serialized bytes
234        let mut cursor = std::io::Cursor::new(binary_data);
235        bucket.put(key_meta, &mut cursor).await.map_err(|e| {
236            anyhow::anyhow!("Failed uploading to bucket / object store {bucket_name}/{key}: {e}")
237        })?;
238
239        Ok(())
240    }
241
242    /// Download and deserialize a struct from NATS object store using bincode
243    pub async fn object_store_download_data<T>(&self, nats_url: &Url) -> anyhow::Result<T>
244    where
245        T: DeserializeOwned,
246    {
247        let (bucket_name, key) = url_to_bucket_and_key(nats_url)?;
248        let bucket = self.get_or_create_bucket(&bucket_name, false).await?;
249
250        let mut obj_reader = bucket.get(&key).await.map_err(|e| {
251            anyhow::anyhow!(
252                "Failed downloading from bucket / object store {bucket_name}/{key}: {e}"
253            )
254        })?;
255
256        // Read all bytes into memory
257        let mut buffer = Vec::new();
258        tokio::io::copy(&mut obj_reader, &mut buffer)
259            .await
260            .map_err(|e| anyhow::anyhow!("Failed reading object data: {e}"))?;
261        tracing::debug!("Downloaded {} bytes from {bucket_name}/{key}", buffer.len());
262
263        // Deserialize from bincode
264        let data = bincode::deserialize(&buffer)
265            .map_err(|e| anyhow::anyhow!("Failed to deserialize data with bincode: {e}"))?;
266
267        Ok(data)
268    }
269}
270
271/// NATS client options
272///
273/// This object uses the builder pattern with default values that are evaluates
274/// from the environment variables if they are not explicitly set by the builder.
275#[derive(Debug, Clone, Builder, Validate)]
276pub struct ClientOptions {
277    #[builder(setter(into), default = "default_server()")]
278    #[validate(custom(function = "validate_nats_server"))]
279    server: String,
280
281    #[builder(default)]
282    auth: NatsAuth,
283}
284
285fn default_server() -> String {
286    if let Ok(server) = std::env::var(env_nats::NATS_SERVER) {
287        return server;
288    }
289
290    "nats://localhost:4222".to_string()
291}
292
293fn validate_nats_server(server: &str) -> Result<(), ValidationError> {
294    if server.starts_with("nats://") {
295        Ok(())
296    } else {
297        Err(ValidationError::new("server must start with 'nats://'"))
298    }
299}
300
301// TODO(jthomson04): We really shouldn't be hardcoding this.
302const NATS_WORKER_THREADS: usize = 4;
303
304impl ClientOptions {
305    /// Create a new [`ClientOptionsBuilder`]
306    pub fn builder() -> ClientOptionsBuilder {
307        ClientOptionsBuilder::default()
308    }
309
310    /// Validate the config and attempt to connection to the NATS server
311    pub async fn connect(self) -> Result<Client> {
312        self.validate()?;
313
314        let client = match self.auth {
315            NatsAuth::UserPass(username, password) => {
316                async_nats::ConnectOptions::with_user_and_password(username, password)
317            }
318            NatsAuth::Token(token) => async_nats::ConnectOptions::with_token(token),
319            NatsAuth::NKey(nkey) => async_nats::ConnectOptions::with_nkey(nkey),
320            NatsAuth::CredentialsFile(path) => {
321                async_nats::ConnectOptions::with_credentials_file(path).await?
322            }
323        };
324
325        // 0 is treated as unset — Duration::from_secs(0) would time out every request immediately.
326        let request_timeout = std::env::var(env_nats::DYN_NATS_REQUEST_TIMEOUT_SECS)
327            .ok()
328            .and_then(|v| v.parse::<u64>().ok())
329            .filter(|&secs| secs > 0)
330            .map(time::Duration::from_secs);
331        let client = match request_timeout {
332            Some(timeout) => client.request_timeout(Some(timeout)),
333            None => client,
334        };
335
336        let (client, _) = build_in_runtime(
337            async move {
338                client
339                    .connect(self.server)
340                    .await
341                    .map_err(|e| anyhow::anyhow!("Failed to connect to NATS: {e}. Verify NATS server is running and accessible."))
342            },
343            NATS_WORKER_THREADS,
344        )
345        .await?;
346
347        let js_ctx = jetstream::new(client.clone());
348
349        Ok(Client { client, js_ctx })
350    }
351}
352
353impl Default for ClientOptions {
354    fn default() -> Self {
355        ClientOptions {
356            server: default_server(),
357            auth: NatsAuth::default(),
358        }
359    }
360}
361
362#[derive(Clone, Eq, PartialEq)]
363pub enum NatsAuth {
364    UserPass(String, String),
365    Token(String),
366    NKey(String),
367    CredentialsFile(PathBuf),
368}
369
370impl std::fmt::Debug for NatsAuth {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        match self {
373            NatsAuth::UserPass(user, _pass) => {
374                write!(f, "UserPass({}, <redacted>)", user)
375            }
376            NatsAuth::Token(_token) => write!(f, "Token(<redacted>)"),
377            NatsAuth::NKey(_nkey) => write!(f, "NKey(<redacted>)"),
378            NatsAuth::CredentialsFile(path) => write!(f, "CredentialsFile({:?})", path),
379        }
380    }
381}
382
383impl Default for NatsAuth {
384    fn default() -> Self {
385        if let (Ok(username), Ok(password)) = (
386            std::env::var(env_nats::auth::NATS_AUTH_USERNAME),
387            std::env::var(env_nats::auth::NATS_AUTH_PASSWORD),
388        ) {
389            return NatsAuth::UserPass(username, password);
390        }
391
392        if let Ok(token) = std::env::var(env_nats::auth::NATS_AUTH_TOKEN) {
393            return NatsAuth::Token(token);
394        }
395
396        if let Ok(nkey) = std::env::var(env_nats::auth::NATS_AUTH_NKEY) {
397            return NatsAuth::NKey(nkey);
398        }
399
400        if let Ok(path) = std::env::var(env_nats::auth::NATS_AUTH_CREDENTIALS_FILE) {
401            return NatsAuth::CredentialsFile(PathBuf::from(path));
402        }
403
404        NatsAuth::UserPass("user".to_string(), "user".to_string())
405    }
406}
407
408/// Extract NATS bucket and key from a nats URL of the form:
409/// nats://host[:port]/bucket/key
410pub fn url_to_bucket_and_key(url: &Url) -> anyhow::Result<(String, String)> {
411    let Some(mut path_segments) = url.path_segments() else {
412        anyhow::bail!("No path in NATS URL: {url}");
413    };
414    let Some(bucket) = path_segments.next() else {
415        anyhow::bail!("No bucket in NATS URL: {url}");
416    };
417    let Some(key) = path_segments.next() else {
418        anyhow::bail!("No key in NATS URL: {url}");
419    };
420    Ok((bucket.to_string(), key.to_string()))
421}
422
423/// A queue implementation using NATS JetStream
424pub struct NatsQueue {
425    /// The name of the stream to use for the queue
426    stream_name: String,
427    /// The NATS server URL
428    nats_server: String,
429    /// Timeout for dequeue operations in seconds
430    dequeue_timeout: time::Duration,
431    /// The NATS client
432    client: Option<Client>,
433    /// The subject pattern used for this queue
434    subject: String,
435    /// The subscriber for pull-based consumption
436    subscriber: Option<jetstream::consumer::PullConsumer>,
437    /// Optional consumer name for broadcast pattern (if None, uses "worker-group")
438    consumer_name: Option<String>,
439    /// Message stream for efficient message consumption
440    message_stream: Option<jetstream::consumer::pull::Stream>,
441}
442
443impl NatsQueue {
444    /// Create a new NatsQueue with the default "worker-group" consumer
445    pub fn new(stream_name: String, nats_server: String, dequeue_timeout: time::Duration) -> Self {
446        // Sanitize stream name to remove path separators (like in Python version)
447        // rupei: are we sure NATs stream name accepts '_'?
448        let sanitized_stream_name = Slug::slugify(&stream_name).to_string();
449        let subject = format!("{sanitized_stream_name}.*");
450
451        Self {
452            stream_name: sanitized_stream_name,
453            nats_server,
454            dequeue_timeout,
455            client: None,
456            subject,
457            subscriber: None,
458            consumer_name: Some("worker-group".to_string()),
459            message_stream: None,
460        }
461    }
462
463    /// Create a new NatsQueue without a consumer (publisher-only mode)
464    pub fn new_without_consumer(
465        stream_name: String,
466        nats_server: String,
467        dequeue_timeout: time::Duration,
468    ) -> Self {
469        let sanitized_stream_name = Slug::slugify(&stream_name).to_string();
470        let subject = format!("{sanitized_stream_name}.*");
471
472        Self {
473            stream_name: sanitized_stream_name,
474            nats_server,
475            dequeue_timeout,
476            client: None,
477            subject,
478            subscriber: None,
479            consumer_name: None,
480            message_stream: None,
481        }
482    }
483
484    /// Create a new NatsQueue with a specific consumer name for broadcast pattern
485    /// Each consumer with a unique name will receive all messages independently
486    pub fn new_with_consumer(
487        stream_name: String,
488        nats_server: String,
489        dequeue_timeout: time::Duration,
490        consumer_name: String,
491    ) -> Self {
492        let sanitized_stream_name = Slug::slugify(&stream_name).to_string();
493        let subject = format!("{sanitized_stream_name}.*");
494
495        Self {
496            stream_name: sanitized_stream_name,
497            nats_server,
498            dequeue_timeout,
499            client: None,
500            subject,
501            subscriber: None,
502            consumer_name: Some(consumer_name),
503            message_stream: None,
504        }
505    }
506
507    /// Connect to the NATS server and set up the stream and consumer
508    pub async fn connect(&mut self) -> Result<()> {
509        self.connect_with_reset(false).await
510    }
511
512    /// Connect to the NATS server and set up the stream and consumer, optionally resetting the stream
513    pub async fn connect_with_reset(&mut self, reset_stream: bool) -> Result<()> {
514        if self.client.is_none() {
515            // Create a new client
516            let client_options = Client::builder().server(self.nats_server.clone()).build()?;
517
518            let client = client_options.connect().await?;
519
520            // messages older than a hour in the stream will be automatically purged
521            let max_age = std::env::var(env_nats::stream::DYN_NATS_STREAM_MAX_AGE)
522                .ok()
523                .and_then(|s| s.parse::<u64>().ok())
524                .map(time::Duration::from_secs)
525                .unwrap_or_else(|| time::Duration::from_secs(60 * 60));
526
527            let stream_config = jetstream::stream::Config {
528                name: self.stream_name.clone(),
529                subjects: vec![self.subject.clone()],
530                max_age,
531                ..Default::default()
532            };
533
534            // Get or create the stream
535            let stream = client
536                .jetstream()
537                .get_or_create_stream(stream_config)
538                .await?;
539
540            log::debug!("Stream {} is ready", self.stream_name);
541
542            // If reset_stream is true, purge all messages from the stream
543            if reset_stream {
544                match stream.purge().await {
545                    Ok(purge_info) => {
546                        log::info!(
547                            "Successfully purged {} messages from NATS stream {}",
548                            purge_info.purged,
549                            self.stream_name
550                        );
551                    }
552                    Err(e) => {
553                        log::warn!("Failed to purge NATS stream '{}': {e}", self.stream_name);
554                    }
555                }
556            }
557
558            // Create persistent subscriber only if consumer_name is set
559            if let Some(ref consumer_name) = self.consumer_name {
560                let consumer_config = jetstream::consumer::pull::Config {
561                    durable_name: Some(consumer_name.clone()),
562                    inactive_threshold: std::time::Duration::from_secs(300), // 5 minutes
563                    ..Default::default()
564                };
565
566                let subscriber = stream.create_consumer(consumer_config).await?;
567
568                // Create the message stream for efficient consumption
569                let message_stream = subscriber.messages().await?;
570
571                self.subscriber = Some(subscriber);
572                self.message_stream = Some(message_stream);
573            }
574
575            self.client = Some(client);
576        }
577
578        Ok(())
579    }
580
581    /// Ensure we have an active connection
582    pub async fn ensure_connection(&mut self) -> Result<()> {
583        if self.client.is_none() {
584            self.connect().await?;
585        }
586        Ok(())
587    }
588
589    /// Close the connection when done
590    pub async fn close(&mut self) -> Result<()> {
591        self.message_stream = None;
592        self.subscriber = None;
593        self.client = None;
594        Ok(())
595    }
596
597    /// Shutdown the consumer by deleting it from the stream and closing the connection
598    /// This permanently removes the consumer from the server
599    ///
600    /// If `consumer_name` is provided, that specific consumer will be deleted instead of the
601    /// current consumer. This allows deletion of other consumers on the same stream.
602    pub async fn shutdown(&mut self, consumer_name: Option<String>) -> Result<()> {
603        // Determine which consumer to delete
604        let target_consumer = consumer_name.as_ref().or(self.consumer_name.as_ref());
605
606        // Warn if deleting our own consumer via explicit parameter
607        if let Some(ref passed_name) = consumer_name
608            && self.consumer_name.as_ref() == Some(passed_name)
609        {
610            log::warn!(
611                "Deleting our own consumer '{}' via explicit consumer_name parameter. \
612                Consider calling shutdown without arguments instead.",
613                passed_name
614            );
615        }
616
617        if let (Some(client), Some(consumer_to_delete)) = (&self.client, target_consumer) {
618            // Get the stream and delete the consumer
619            let stream = client.jetstream().get_stream(&self.stream_name).await?;
620            stream
621                .delete_consumer(consumer_to_delete)
622                .await
623                .map_err(|e| {
624                    anyhow::anyhow!("Failed to delete consumer {}: {}", consumer_to_delete, e)
625                })?;
626            log::debug!(
627                "Deleted consumer {} from stream {}",
628                consumer_to_delete,
629                self.stream_name
630            );
631        } else {
632            log::debug!(
633                "Cannot shutdown consumer: client or target consumer is None (client: {:?}, target_consumer: {:?})",
634                self.client.is_some(),
635                target_consumer.is_some()
636            );
637        }
638
639        // Only close the connection if we deleted our own consumer
640        if consumer_name.is_none() {
641            self.close().await
642        } else {
643            Ok(())
644        }
645    }
646
647    /// Count the number of consumers for the stream
648    pub async fn count_consumers(&mut self) -> Result<usize> {
649        self.ensure_connection().await?;
650
651        if let Some(client) = &self.client {
652            let mut stream = client.jetstream().get_stream(&self.stream_name).await?;
653            let info = stream.info().await?;
654            Ok(info.state.consumer_count)
655        } else {
656            Err(anyhow::anyhow!("Client not connected"))
657        }
658    }
659
660    /// List all consumer names for the stream
661    pub async fn list_consumers(&mut self) -> Result<Vec<String>> {
662        self.ensure_connection().await?;
663
664        if let Some(client) = &self.client {
665            client.list_consumers(&self.stream_name).await
666        } else {
667            Err(anyhow::anyhow!("Client not connected"))
668        }
669    }
670
671    /// Enqueue a task using the provided data
672    pub async fn enqueue_task(&mut self, task_data: Bytes) -> Result<()> {
673        self.ensure_connection().await?;
674
675        if let Some(client) = &self.client {
676            let subject = format!("{}.queue", self.stream_name);
677            client.jetstream().publish(subject, task_data).await?;
678            Ok(())
679        } else {
680            Err(anyhow::anyhow!("Client not connected"))
681        }
682    }
683
684    /// Dequeue and return a task as raw bytes
685    pub async fn dequeue_task(&mut self, timeout: Option<time::Duration>) -> Result<Option<Bytes>> {
686        self.ensure_connection().await?;
687
688        let Some(ref mut stream) = self.message_stream else {
689            return Err(anyhow::anyhow!("Message stream not initialized"));
690        };
691
692        let timeout_duration = timeout.unwrap_or(self.dequeue_timeout);
693
694        // Try to get next message from the stream with timeout
695        let message = tokio::time::timeout(timeout_duration, stream.next()).await;
696
697        match message {
698            Ok(Some(Ok(msg))) => {
699                msg.ack()
700                    .await
701                    .map_err(|e| anyhow::anyhow!("Failed to ack message: {}", e))?;
702                Ok(Some(msg.payload.clone()))
703            }
704
705            Ok(Some(Err(e))) => Err(anyhow::anyhow!("Failed to get message from stream: {}", e)),
706
707            Ok(None) => Err(anyhow::anyhow!("Message stream ended unexpectedly")),
708
709            // Timeout - no messages available
710            Err(_) => Ok(None),
711        }
712    }
713
714    /// Get the number of messages currently in the queue
715    pub async fn get_queue_size(&mut self) -> Result<u64> {
716        self.ensure_connection().await?;
717
718        if let Some(client) = &self.client {
719            // Get consumer info to get pending messages count
720            let stream = client.jetstream().get_stream(&self.stream_name).await?;
721            let consumer_name = self
722                .consumer_name
723                .clone()
724                .unwrap_or_else(|| "worker-group".to_string());
725            let mut consumer: jetstream::consumer::PullConsumer = stream
726                .get_consumer(&consumer_name)
727                .await
728                .map_err(|e| anyhow::anyhow!("Failed to get consumer: {}", e))?;
729            let info = consumer.info().await?;
730
731            Ok(info.num_pending)
732        } else {
733            Err(anyhow::anyhow!("Client not connected"))
734        }
735    }
736
737    /// Get the total number of messages currently in the stream
738    pub async fn get_stream_messages(&mut self) -> Result<u64> {
739        self.ensure_connection().await?;
740
741        if let Some(client) = &self.client {
742            let mut stream = client.jetstream().get_stream(&self.stream_name).await?;
743            let info = stream.info().await?;
744            Ok(info.state.messages)
745        } else {
746            Err(anyhow::anyhow!("Client not connected"))
747        }
748    }
749
750    /// Purge messages from the stream up to (but not including) the specified sequence number
751    /// This permanently removes messages and affects all consumers of the stream
752    pub async fn purge_up_to_sequence(&self, sequence: u64) -> Result<()> {
753        if let Some(client) = &self.client {
754            let stream = client.jetstream().get_stream(&self.stream_name).await?;
755
756            // NOTE: this purge excludes the sequence itself
757            // https://docs.rs/nats/latest/nats/jetstream/struct.PurgeRequest.html
758            stream.purge().sequence(sequence).await.map_err(|e| {
759                anyhow::anyhow!("Failed to purge stream up to sequence {}: {}", sequence, e)
760            })?;
761
762            log::debug!(
763                "Purged stream {} up to sequence {}",
764                self.stream_name,
765                sequence
766            );
767            Ok(())
768        } else {
769            Err(anyhow::anyhow!("Client not connected"))
770        }
771    }
772
773    /// Purge messages from the stream up to the minimum acknowledged sequence across all consumers
774    /// This finds the lowest acknowledged sequence number across all consumers and purges up to that point
775    pub async fn purge_acknowledged(&mut self) -> Result<()> {
776        self.ensure_connection().await?;
777
778        let Some(client) = &self.client else {
779            return Err(anyhow::anyhow!("Client not connected"));
780        };
781
782        let stream = client.jetstream().get_stream(&self.stream_name).await?;
783
784        // Get all consumer names for the stream
785        let consumer_names: Vec<String> = stream
786            .consumer_names()
787            .try_collect()
788            .await
789            .map_err(|e| anyhow::anyhow!("Failed to list consumers: {}", e))?;
790
791        if consumer_names.is_empty() {
792            log::debug!("No consumers found for stream {}", self.stream_name);
793            return Ok(());
794        }
795
796        // Find the minimum acknowledged sequence across all consumers
797        let mut min_ack_sequence = u64::MAX;
798
799        for consumer_name in &consumer_names {
800            let mut consumer: jetstream::consumer::PullConsumer = stream
801                .get_consumer(consumer_name)
802                .await
803                .map_err(|e| anyhow::anyhow!("Failed to get consumer {}: {}", consumer_name, e))?;
804
805            let info = consumer.info().await.map_err(|e| {
806                anyhow::anyhow!("Failed to get consumer info for {}: {}", consumer_name, e)
807            })?;
808
809            // The ack_floor contains the stream sequence of the highest contiguously acknowledged message
810            // If stream_sequence is 0, it means no messages have been acknowledged yet
811            if info.ack_floor.stream_sequence > 0 {
812                min_ack_sequence = min_ack_sequence.min(info.ack_floor.stream_sequence);
813                log::debug!(
814                    "Consumer {} has ack_floor at sequence {}",
815                    consumer_name,
816                    info.ack_floor.stream_sequence
817                );
818            }
819        }
820
821        // Only purge if we found a valid minimum acknowledged sequence
822        if min_ack_sequence < u64::MAX && min_ack_sequence > 0 {
823            // Purge up to (but not including) the minimum acknowledged sequence + 1
824            // We add 1 because we want to include the minimum acknowledged message in the purge
825            let purge_sequence = min_ack_sequence + 1;
826
827            self.purge_up_to_sequence(purge_sequence).await?;
828
829            log::debug!(
830                "Purged stream {} up to acknowledged sequence {} (purged up to sequence {})",
831                self.stream_name,
832                min_ack_sequence,
833                purge_sequence
834            );
835        } else {
836            log::debug!(
837                "No messages to purge for stream {} (min_ack_sequence: {})",
838                self.stream_name,
839                min_ack_sequence
840            );
841        }
842
843        Ok(())
844    }
845}
846
847impl NatsQueue {
848    pub fn event_subject(&self) -> String {
849        self.stream_name.clone()
850    }
851
852    pub async fn publish_event(
853        &self,
854        event_name: impl AsRef<str> + Send + Sync,
855        event: &(impl Serialize + Send + Sync),
856    ) -> Result<()> {
857        let bytes = serde_json::to_vec(event)?;
858        self.publish_event_bytes(event_name, bytes).await
859    }
860
861    pub async fn publish_event_bytes(
862        &self,
863        event_name: impl AsRef<str> + Send + Sync,
864        bytes: Vec<u8>,
865    ) -> Result<()> {
866        let subject = format!("{}.{}", self.event_subject(), event_name.as_ref());
867
868        // Note: enqueue_task requires &mut self, but EventPublisher requires &self
869        // We need to ensure the client is connected and use it directly
870        if let Some(client) = &self.client {
871            client.jetstream().publish(subject, bytes.into()).await?;
872            Ok(())
873        } else {
874            Err(anyhow::anyhow!("Client not connected"))
875        }
876    }
877}
878
879/// The NATS subject / inbox to talk to an instance on.
880/// TODO: Do we need to sanitize the names?
881pub fn instance_subject(endpoint_id: &EndpointId, instance_id: u64) -> String {
882    format!(
883        "{}_{}.{}-{:x}",
884        endpoint_id.namespace, endpoint_id.component, endpoint_id.name, instance_id,
885    )
886}
887
888#[cfg(test)]
889mod tests {
890
891    use super::*;
892    use figment::Jail;
893    use serde::{Deserialize, Serialize};
894
895    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
896    struct TestData {
897        id: u32,
898        name: String,
899        values: Vec<f64>,
900    }
901
902    #[test]
903    fn test_client_options_builder() {
904        Jail::expect_with(|_jail| {
905            let opts = ClientOptions::builder().build();
906            assert!(opts.is_ok());
907            Ok(())
908        });
909
910        Jail::expect_with(|jail| {
911            jail.set_env(env_nats::NATS_SERVER, "nats://localhost:5222");
912            jail.set_env(env_nats::auth::NATS_AUTH_USERNAME, "user");
913            jail.set_env(env_nats::auth::NATS_AUTH_PASSWORD, "pass");
914
915            let opts = ClientOptions::builder().build();
916            assert!(opts.is_ok());
917            let opts = opts.unwrap();
918
919            assert_eq!(opts.server, "nats://localhost:5222");
920            assert_eq!(
921                opts.auth,
922                NatsAuth::UserPass("user".to_string(), "pass".to_string())
923            );
924
925            Ok(())
926        });
927
928        Jail::expect_with(|jail| {
929            jail.set_env(env_nats::NATS_SERVER, "nats://localhost:5222");
930            jail.set_env(env_nats::auth::NATS_AUTH_USERNAME, "user");
931            jail.set_env(env_nats::auth::NATS_AUTH_PASSWORD, "pass");
932
933            let opts = ClientOptions::builder()
934                .server("nats://localhost:6222")
935                .auth(NatsAuth::Token("token".to_string()))
936                .build();
937            assert!(opts.is_ok());
938            let opts = opts.unwrap();
939
940            assert_eq!(opts.server, "nats://localhost:6222");
941            assert_eq!(opts.auth, NatsAuth::Token("token".to_string()));
942
943            Ok(())
944        });
945    }
946
947    // Integration test for object store data operations using bincode
948    #[tokio::test]
949    #[ignore] // Requires NATS server to be running
950    async fn test_object_store_data_operations() {
951        // Create test data
952        let test_data = TestData {
953            id: 42,
954            name: "test_item".to_string(),
955            values: vec![1.0, 2.5, 3.7, 4.2],
956        };
957
958        // Set up client
959        let client_options = ClientOptions::builder()
960            .server("nats://localhost:4222")
961            .build()
962            .expect("Failed to build client options");
963
964        let client = client_options
965            .connect()
966            .await
967            .expect("Failed to connect to NATS");
968
969        // Test URL (using .bin extension to indicate binary format)
970        let url =
971            Url::parse("nats://localhost/test-bucket/test-data.bin").expect("Failed to parse URL");
972
973        // Upload the data
974        client
975            .object_store_upload_data(&test_data, &url)
976            .await
977            .expect("Failed to upload data");
978
979        // Download the data
980        let downloaded_data: TestData = client
981            .object_store_download_data(&url)
982            .await
983            .expect("Failed to download data");
984
985        // Verify the data matches
986        assert_eq!(test_data, downloaded_data);
987
988        // Clean up
989        client
990            .object_store_delete_bucket("test-bucket")
991            .await
992            .expect("Failed to delete bucket");
993    }
994
995    // Integration test for broadcast pattern with purging
996    #[tokio::test]
997    #[ignore]
998    async fn test_nats_queue_broadcast_with_purge() {
999        use uuid::Uuid;
1000
1001        // Create unique stream name for this test
1002        let stream_name = format!("test-broadcast-{}", Uuid::new_v4());
1003        let nats_server = "nats://localhost:4222".to_string();
1004        let timeout = time::Duration::from_secs(0);
1005
1006        // Connect to NATS client first to delete stream if it exists
1007        let client_options = Client::builder()
1008            .server(nats_server.clone())
1009            .build()
1010            .expect("Failed to build client options");
1011
1012        let client = client_options
1013            .connect()
1014            .await
1015            .expect("Failed to connect to NATS");
1016
1017        // Delete the stream if it exists (to ensure clean start)
1018        let _ = client.jetstream().delete_stream(&stream_name).await;
1019
1020        // Create two consumers with different names for the same stream
1021        let consumer1_name = format!("consumer-{}", Uuid::new_v4());
1022        let consumer2_name = format!("consumer-{}", Uuid::new_v4());
1023
1024        let mut queue1 = NatsQueue::new_with_consumer(
1025            stream_name.clone(),
1026            nats_server.clone(),
1027            timeout,
1028            consumer1_name,
1029        );
1030
1031        // Connect queue1 first (it will create the stream)
1032        queue1.connect().await.expect("Failed to connect queue1");
1033
1034        // Send 4 messages using the EventPublisher trait
1035        let message_strings = [
1036            "message1".to_string(),
1037            "message2".to_string(),
1038            "message3".to_string(),
1039            "message4".to_string(),
1040        ];
1041
1042        // Publish messages using NatsQueue
1043        for (idx, msg) in message_strings.iter().enumerate() {
1044            queue1
1045                .publish_event("queue", msg)
1046                .await
1047                .unwrap_or_else(|_| panic!("Failed to publish message {}", idx + 1));
1048        }
1049
1050        // Convert messages to JSON-serialized Bytes for comparison
1051        let messages: Vec<Bytes> = message_strings
1052            .iter()
1053            .map(|s| Bytes::from(serde_json::to_vec(s).unwrap()))
1054            .collect();
1055
1056        // Give JetStream a moment to persist the messages
1057        tokio::time::sleep(time::Duration::from_millis(100)).await;
1058
1059        // Now create and connect queue2 and queue3 AFTER messages are published (to test persistence)
1060        let mut queue2 = NatsQueue::new_with_consumer(
1061            stream_name.clone(),
1062            nats_server.clone(),
1063            timeout,
1064            consumer2_name,
1065        );
1066
1067        // Create a third queue without consumer (publisher-only)
1068        let mut queue3 =
1069            NatsQueue::new_without_consumer(stream_name.clone(), nats_server.clone(), timeout);
1070
1071        // Connect queue2 and queue3 after messages are already published
1072        queue2.connect().await.expect("Failed to connect queue2");
1073        queue3.connect().await.expect("Failed to connect queue3");
1074
1075        // Purge the first two messages (sequence 1 and 2)
1076        // Note: JetStream sequences start at 1, and purge is exclusive of the sequence number
1077        queue1
1078            .purge_up_to_sequence(3)
1079            .await
1080            .expect("Failed to purge messages");
1081
1082        // Give JetStream a moment to process the purge
1083        tokio::time::sleep(time::Duration::from_millis(100)).await;
1084
1085        // Consumer 1 dequeues one message (message3)
1086        let msg3_consumer1 = queue1
1087            .dequeue_task(Some(time::Duration::from_millis(500)))
1088            .await
1089            .expect("Failed to dequeue from queue1");
1090        assert_eq!(
1091            msg3_consumer1,
1092            Some(messages[2].clone()),
1093            "Consumer 1 should get message3"
1094        );
1095
1096        // Give JetStream a moment to process acknowledgments
1097        tokio::time::sleep(time::Duration::from_millis(100)).await;
1098
1099        // Now run purge_acknowledged
1100        // At this point:
1101        // - Consumer 1 has ack'd message 3 (ack_floor = 3)
1102        // - Consumer 2 hasn't consumed anything yet (ack_floor = 0)
1103        // - Min ack_floor = 0, so nothing will be purged
1104        queue1
1105            .purge_acknowledged()
1106            .await
1107            .expect("Failed to purge acknowledged messages");
1108
1109        // Give JetStream a moment to process the purge
1110        tokio::time::sleep(time::Duration::from_millis(100)).await;
1111
1112        // Now collect remaining messages from both consumers
1113        let mut consumer1_remaining = Vec::new();
1114        let mut consumer2_remaining = Vec::new();
1115
1116        // Collect remaining messages from consumer 1
1117        while let Some(msg) = queue1
1118            .dequeue_task(None)
1119            .await
1120            .expect("Failed to dequeue from queue1")
1121        {
1122            consumer1_remaining.push(msg);
1123        }
1124
1125        // Collect remaining messages from consumer 2
1126        while let Some(msg) = queue2
1127            .dequeue_task(None)
1128            .await
1129            .expect("Failed to dequeue from queue2")
1130        {
1131            consumer2_remaining.push(msg);
1132        }
1133
1134        // Verify consumer 1 gets 1 remaining message (message4)
1135        assert_eq!(
1136            consumer1_remaining.len(),
1137            1,
1138            "Consumer 1 should have 1 remaining message"
1139        );
1140        assert_eq!(
1141            consumer1_remaining[0], messages[3],
1142            "Consumer 1 should get message4"
1143        );
1144
1145        // Verify consumer 2 gets 2 messages (message3 and message4)
1146        assert_eq!(
1147            consumer2_remaining.len(),
1148            2,
1149            "Consumer 2 should have 2 messages"
1150        );
1151        assert_eq!(
1152            consumer2_remaining[0], messages[2],
1153            "Consumer 2 should get message3"
1154        );
1155        assert_eq!(
1156            consumer2_remaining[1], messages[3],
1157            "Consumer 2 should get message4"
1158        );
1159
1160        // Test consumer count and shutdown behavior
1161        // First verify via consumer 1 that there are two consumers
1162        let consumer_count = queue1
1163            .count_consumers()
1164            .await
1165            .expect("Failed to count consumers");
1166        assert_eq!(consumer_count, 2, "Should have 2 consumers initially");
1167
1168        // Close consumer 1 and verify via consumer 2 that there are still two consumers
1169        queue1.close().await.expect("Failed to close queue1");
1170
1171        let consumer_count = queue2
1172            .count_consumers()
1173            .await
1174            .expect("Failed to count consumers");
1175        assert_eq!(
1176            consumer_count, 2,
1177            "Should still have 2 consumers after closing queue1"
1178        );
1179
1180        // Reconnect queue1 to be able to shutdown
1181        queue1.connect().await.expect("Failed to reconnect queue1");
1182
1183        // Shutdown consumer 1 and verify via consumer 2 that there is only one consumer left
1184        queue1
1185            .shutdown(None)
1186            .await
1187            .expect("Failed to shutdown queue1");
1188
1189        let consumer_count = queue2
1190            .count_consumers()
1191            .await
1192            .expect("Failed to count consumers");
1193        assert_eq!(
1194            consumer_count, 1,
1195            "Should have only 1 consumer after shutting down queue1"
1196        );
1197
1198        // Clean up by deleting the stream
1199        client
1200            .jetstream()
1201            .delete_stream(&stream_name)
1202            .await
1203            .expect("Failed to delete test stream");
1204    }
1205}