1use 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 pub fn builder() -> ClientOptionsBuilder {
71 ClientOptionsBuilder::default()
72 }
73
74 pub fn client(&self) -> &client::Client {
76 &self.client
77 }
78
79 pub fn jetstream(&self) -> &jetstream::Context {
81 &self.js_ctx
82 }
83
84 pub fn addr(&self) -> String {
86 let info = self.client.server_info();
87 format!("{}:{}", info.host, info.port)
88 }
89
90 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 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 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 self.client
129 .publish_with_reply(subject, reply_subject, "".into())
130 .await?;
131
132 Ok(subscription)
133 }
134
135 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 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 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 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 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 pub async fn object_store_upload_data<T>(&self, data: &T, nats_url: &Url) -> anyhow::Result<()>
232 where
233 T: Serialize,
234 {
235 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 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 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 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 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#[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 #[builder(default = "default_nats_tls_ca_cert_path()")]
304 tls_ca_cert_path: Option<PathBuf>,
305
306 #[builder(default = "default_nats_tls_client_cert_path()")]
309 tls_client_cert_path: Option<PathBuf>,
310
311 #[builder(default = "default_nats_tls_client_key_path()")]
313 tls_client_key_path: Option<PathBuf>,
314
315 #[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
360const 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 pub fn builder() -> ClientOptionsBuilder {
493 ClientOptionsBuilder::default()
494 }
495
496 pub async fn connect(self) -> Result<Client> {
498 self.connect_with_cancellation(CancellationToken::new())
499 .await
500 }
501
502 pub async fn connect_with_cancellation(self, token: CancellationToken) -> Result<Client> {
504 self.validate()?;
505
506 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 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 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 if custom_tls || tls_url {
566 let _ = rustls::crypto::ring::default_provider().install_default();
567 }
568
569 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 options = options.require_tls(true);
585 }
586
587 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 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
672pub 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
687pub struct NatsQueue {
689 stream_name: String,
691 nats_server: String,
693 dequeue_timeout: time::Duration,
695 client: Option<Client>,
697 subject: String,
699 subscriber: Option<jetstream::consumer::PullConsumer>,
701 consumer_name: Option<String>,
703 message_stream: Option<jetstream::consumer::pull::Stream>,
705}
706
707impl NatsQueue {
708 pub fn new(stream_name: String, nats_server: String, dequeue_timeout: time::Duration) -> Self {
710 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 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 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 pub async fn connect(&mut self) -> Result<()> {
773 self.connect_with_reset(false).await
774 }
775
776 pub async fn connect_with_reset(&mut self, reset_stream: bool) -> Result<()> {
778 if self.client.is_none() {
779 let client_options = Client::builder().server(self.nats_server.clone()).build()?;
781
782 let client = client_options.connect().await?;
783
784 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 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 {
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 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), ..Default::default()
828 };
829
830 let subscriber = stream.create_consumer(consumer_config).await?;
831
832 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 pub async fn ensure_connection(&mut self) -> Result<()> {
847 if self.client.is_none() {
848 self.connect().await?;
849 }
850 Ok(())
851 }
852
853 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 pub async fn shutdown(&mut self, consumer_name: Option<String>) -> Result<()> {
867 let target_consumer = consumer_name.as_ref().or(self.consumer_name.as_ref());
869
870 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 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 if consumer_name.is_none() {
905 self.close().await
906 } else {
907 Ok(())
908 }
909 }
910
911 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 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 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 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 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 Err(_) => Ok(None),
975 }
976 }
977
978 pub async fn get_queue_size(&mut self) -> Result<u64> {
980 self.ensure_connection().await?;
981
982 if let Some(client) = &self.client {
983 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 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 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 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 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 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 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 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 if min_ack_sequence < u64::MAX && min_ack_sequence > 0 {
1087 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 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
1143pub 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 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 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 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 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 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 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 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 #[tokio::test]
1544 #[ignore] async fn test_object_store_data_operations() {
1546 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 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 let url =
1566 Url::parse("nats://localhost/test-bucket/test-data.bin").expect("Failed to parse URL");
1567
1568 client
1570 .object_store_upload_data(&test_data, &url)
1571 .await
1572 .expect("Failed to upload data");
1573
1574 let downloaded_data: TestData = client
1576 .object_store_download_data(&url)
1577 .await
1578 .expect("Failed to download data");
1579
1580 assert_eq!(test_data, downloaded_data);
1582
1583 client
1585 .object_store_delete_bucket("test-bucket")
1586 .await
1587 .expect("Failed to delete bucket");
1588 }
1589
1590 #[tokio::test]
1592 #[ignore]
1593 async fn test_nats_queue_broadcast_with_purge() {
1594 use uuid::Uuid;
1595
1596 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 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 let _ = client.jetstream().delete_stream(&stream_name).await;
1614
1615 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 queue1.connect().await.expect("Failed to connect queue1");
1628
1629 let message_strings = [
1631 "message1".to_string(),
1632 "message2".to_string(),
1633 "message3".to_string(),
1634 "message4".to_string(),
1635 ];
1636
1637 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 let messages: Vec<Bytes> = message_strings
1647 .iter()
1648 .map(|s| Bytes::from(serde_json::to_vec(s).unwrap()))
1649 .collect();
1650
1651 tokio::time::sleep(time::Duration::from_millis(100)).await;
1653
1654 let mut queue2 = NatsQueue::new_with_consumer(
1656 stream_name.clone(),
1657 nats_server.clone(),
1658 timeout,
1659 consumer2_name,
1660 );
1661
1662 let mut queue3 =
1664 NatsQueue::new_without_consumer(stream_name.clone(), nats_server.clone(), timeout);
1665
1666 queue2.connect().await.expect("Failed to connect queue2");
1668 queue3.connect().await.expect("Failed to connect queue3");
1669
1670 queue1
1673 .purge_up_to_sequence(3)
1674 .await
1675 .expect("Failed to purge messages");
1676
1677 tokio::time::sleep(time::Duration::from_millis(100)).await;
1679
1680 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 tokio::time::sleep(time::Duration::from_millis(100)).await;
1693
1694 queue1
1700 .purge_acknowledged()
1701 .await
1702 .expect("Failed to purge acknowledged messages");
1703
1704 tokio::time::sleep(time::Duration::from_millis(100)).await;
1706
1707 let mut consumer1_remaining = Vec::new();
1709 let mut consumer2_remaining = Vec::new();
1710
1711 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 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 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 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 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 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 queue1.connect().await.expect("Failed to reconnect queue1");
1777
1778 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 client
1795 .jetstream()
1796 .delete_stream(&stream_name)
1797 .await
1798 .expect("Failed to delete test stream");
1799 }
1800}