1use 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 pub fn builder() -> ClientOptionsBuilder {
57 ClientOptionsBuilder::default()
58 }
59
60 pub fn client(&self) -> &client::Client {
62 &self.client
63 }
64
65 pub fn jetstream(&self) -> &jetstream::Context {
67 &self.js_ctx
68 }
69
70 pub fn addr(&self) -> String {
72 let info = self.client.server_info();
73 format!("{}:{}", info.host, info.port)
74 }
75
76 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 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 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 self.client
115 .publish_with_reply(subject, reply_subject, "".into())
116 .await?;
117
118 Ok(subscription)
119 }
120
121 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 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 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 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 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 pub async fn object_store_upload_data<T>(&self, data: &T, nats_url: &Url) -> anyhow::Result<()>
218 where
219 T: Serialize,
220 {
221 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 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 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 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 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#[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
301const NATS_WORKER_THREADS: usize = 4;
303
304impl ClientOptions {
305 pub fn builder() -> ClientOptionsBuilder {
307 ClientOptionsBuilder::default()
308 }
309
310 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 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
408pub 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
423pub struct NatsQueue {
425 stream_name: String,
427 nats_server: String,
429 dequeue_timeout: time::Duration,
431 client: Option<Client>,
433 subject: String,
435 subscriber: Option<jetstream::consumer::PullConsumer>,
437 consumer_name: Option<String>,
439 message_stream: Option<jetstream::consumer::pull::Stream>,
441}
442
443impl NatsQueue {
444 pub fn new(stream_name: String, nats_server: String, dequeue_timeout: time::Duration) -> Self {
446 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 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 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 pub async fn connect(&mut self) -> Result<()> {
509 self.connect_with_reset(false).await
510 }
511
512 pub async fn connect_with_reset(&mut self, reset_stream: bool) -> Result<()> {
514 if self.client.is_none() {
515 let client_options = Client::builder().server(self.nats_server.clone()).build()?;
517
518 let client = client_options.connect().await?;
519
520 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 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 {
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 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), ..Default::default()
564 };
565
566 let subscriber = stream.create_consumer(consumer_config).await?;
567
568 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 pub async fn ensure_connection(&mut self) -> Result<()> {
583 if self.client.is_none() {
584 self.connect().await?;
585 }
586 Ok(())
587 }
588
589 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 pub async fn shutdown(&mut self, consumer_name: Option<String>) -> Result<()> {
603 let target_consumer = consumer_name.as_ref().or(self.consumer_name.as_ref());
605
606 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 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 if consumer_name.is_none() {
641 self.close().await
642 } else {
643 Ok(())
644 }
645 }
646
647 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 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 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 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 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 Err(_) => Ok(None),
711 }
712 }
713
714 pub async fn get_queue_size(&mut self) -> Result<u64> {
716 self.ensure_connection().await?;
717
718 if let Some(client) = &self.client {
719 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 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 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 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 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 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 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 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 if min_ack_sequence < u64::MAX && min_ack_sequence > 0 {
823 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 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
879pub 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 #[tokio::test]
949 #[ignore] async fn test_object_store_data_operations() {
951 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 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 let url =
971 Url::parse("nats://localhost/test-bucket/test-data.bin").expect("Failed to parse URL");
972
973 client
975 .object_store_upload_data(&test_data, &url)
976 .await
977 .expect("Failed to upload data");
978
979 let downloaded_data: TestData = client
981 .object_store_download_data(&url)
982 .await
983 .expect("Failed to download data");
984
985 assert_eq!(test_data, downloaded_data);
987
988 client
990 .object_store_delete_bucket("test-bucket")
991 .await
992 .expect("Failed to delete bucket");
993 }
994
995 #[tokio::test]
997 #[ignore]
998 async fn test_nats_queue_broadcast_with_purge() {
999 use uuid::Uuid;
1000
1001 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 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 let _ = client.jetstream().delete_stream(&stream_name).await;
1019
1020 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 queue1.connect().await.expect("Failed to connect queue1");
1033
1034 let message_strings = [
1036 "message1".to_string(),
1037 "message2".to_string(),
1038 "message3".to_string(),
1039 "message4".to_string(),
1040 ];
1041
1042 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 let messages: Vec<Bytes> = message_strings
1052 .iter()
1053 .map(|s| Bytes::from(serde_json::to_vec(s).unwrap()))
1054 .collect();
1055
1056 tokio::time::sleep(time::Duration::from_millis(100)).await;
1058
1059 let mut queue2 = NatsQueue::new_with_consumer(
1061 stream_name.clone(),
1062 nats_server.clone(),
1063 timeout,
1064 consumer2_name,
1065 );
1066
1067 let mut queue3 =
1069 NatsQueue::new_without_consumer(stream_name.clone(), nats_server.clone(), timeout);
1070
1071 queue2.connect().await.expect("Failed to connect queue2");
1073 queue3.connect().await.expect("Failed to connect queue3");
1074
1075 queue1
1078 .purge_up_to_sequence(3)
1079 .await
1080 .expect("Failed to purge messages");
1081
1082 tokio::time::sleep(time::Duration::from_millis(100)).await;
1084
1085 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 tokio::time::sleep(time::Duration::from_millis(100)).await;
1098
1099 queue1
1105 .purge_acknowledged()
1106 .await
1107 .expect("Failed to purge acknowledged messages");
1108
1109 tokio::time::sleep(time::Duration::from_millis(100)).await;
1111
1112 let mut consumer1_remaining = Vec::new();
1114 let mut consumer2_remaining = Vec::new();
1115
1116 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 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 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 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 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 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 queue1.connect().await.expect("Failed to reconnect queue1");
1182
1183 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 client
1200 .jetstream()
1201 .delete_stream(&stream_name)
1202 .await
1203 .expect("Failed to delete test stream");
1204 }
1205}