1use mongodb::{
15 bson::{doc, Document},
16 error::Error as DriverError,
17 options::ClientOptions,
18 results::{DeleteResult, InsertManyResult, InsertOneResult, UpdateResult},
19 Client, ClientSession, Collection, Database,
20};
21use serde::Serialize;
22use std::{
23 error::Error,
24 fmt,
25 future::Future,
26 hash::Hash,
27 sync::{
28 atomic::{AtomicU64, Ordering},
29 Mutex,
30 },
31 time::{Duration, Instant},
32};
33
34use crate::{
35 cache::jittered_ttl, CacheStats, CounterVec, HistogramOptions, HistogramVec, MemoryCache,
36 Metrics, MetricsError, SingleFlight, SingleFlightError, VectorOptions,
37};
38
39#[cfg(feature = "telemetry")]
40use crate::{TelemetrySpan, TelemetrySpanKind};
41
42#[derive(Clone)]
44pub struct MongoStoreMetrics {
45 operations: CounterVec,
46 duration: HistogramVec,
47}
48
49impl MongoStoreMetrics {
50 pub fn register(metrics: &Metrics) -> Result<Self, MetricsError> {
51 let labels = ["operation", "kind", "outcome"];
52 Ok(Self {
53 operations: metrics.counter_vec(
54 VectorOptions::new("operations_total", "Completed MongoDB store operations")
55 .with_namespace("rust_zero")
56 .with_subsystem("mongo")
57 .with_labels(labels),
58 )?,
59 duration: metrics.histogram_vec(
60 HistogramOptions::new(
61 "operation_duration_seconds",
62 "MongoDB store operation latency",
63 )
64 .with_vector_options(
65 VectorOptions::new(
66 "operation_duration_seconds",
67 "MongoDB store operation latency",
68 )
69 .with_namespace("rust_zero")
70 .with_subsystem("mongo")
71 .with_labels(labels),
72 ),
73 )?,
74 })
75 }
76
77 fn observe(&self, operation: &str, kind: MongoOperationKind, outcome: &str, elapsed: Duration) {
78 let labels = [operation, kind.as_str(), outcome];
79 let _ = self.operations.inc(&labels);
80 let _ = self.duration.observe(elapsed.as_secs_f64(), &labels);
81 }
82}
83
84#[derive(Debug, Clone, Copy)]
85enum MongoOperationKind {
86 Query,
87 Execute,
88 BulkInsert,
89}
90
91impl MongoOperationKind {
92 fn as_str(self) -> &'static str {
93 match self {
94 Self::Query => "query",
95 Self::Execute => "execute",
96 Self::BulkInsert => "bulk_insert",
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct MongoStoreConfig {
103 pub uri: String,
104 pub database: String,
105 pub application_name: Option<String>,
106 pub min_pool_size: Option<u32>,
107 pub max_pool_size: Option<u32>,
108 pub connect_timeout: Option<Duration>,
109 pub server_selection_timeout: Option<Duration>,
110}
111
112impl MongoStoreConfig {
113 pub fn new(uri: impl Into<String>, database: impl Into<String>) -> Self {
114 Self {
115 uri: uri.into(),
116 database: database.into(),
117 application_name: None,
118 min_pool_size: None,
119 max_pool_size: None,
120 connect_timeout: Some(Duration::from_secs(10)),
121 server_selection_timeout: Some(Duration::from_secs(10)),
122 }
123 }
124
125 pub fn with_application_name(mut self, name: impl Into<String>) -> Self {
126 self.application_name = Some(name.into());
127 self
128 }
129
130 pub fn with_pool_size(mut self, min: u32, max: u32) -> Self {
131 assert!(max > 0, "MongoDB maximum pool size must be positive");
132 assert!(
133 min <= max,
134 "MongoDB minimum pool size cannot exceed maximum"
135 );
136 self.min_pool_size = Some(min);
137 self.max_pool_size = Some(max);
138 self
139 }
140
141 pub fn with_timeouts(mut self, connect: Duration, server_selection: Duration) -> Self {
142 assert!(
143 !connect.is_zero(),
144 "MongoDB connect timeout must be positive"
145 );
146 assert!(
147 !server_selection.is_zero(),
148 "MongoDB server selection timeout must be positive"
149 );
150 self.connect_timeout = Some(connect);
151 self.server_selection_timeout = Some(server_selection);
152 self
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub struct MongoCacheConfig {
159 pub capacity: usize,
160 pub ttl: Duration,
161 pub not_found_ttl: Option<Duration>,
162 pub ttl_jitter: Duration,
163}
164
165impl MongoCacheConfig {
166 pub fn new(capacity: usize, ttl: Duration) -> Self {
167 assert!(capacity > 0, "MongoDB cache capacity must be positive");
168 assert!(!ttl.is_zero(), "MongoDB cache TTL must be positive");
169 Self {
170 capacity,
171 ttl,
172 not_found_ttl: Some(ttl),
173 ttl_jitter: Duration::ZERO,
174 }
175 }
176
177 pub fn with_not_found_ttl(mut self, ttl: Option<Duration>) -> Self {
179 assert!(
180 ttl.is_none_or(|ttl| !ttl.is_zero()),
181 "MongoDB not-found cache TTL must be positive"
182 );
183 self.not_found_ttl = ttl;
184 self
185 }
186
187 pub fn with_ttl_jitter(mut self, jitter: Duration) -> Self {
189 self.ttl_jitter = jitter;
190 self
191 }
192}
193
194#[derive(Debug)]
195pub enum MongoStoreError {
196 InvalidDatabase,
197 InvalidBatchSize,
198 NotFound { entity: String },
199 Driver(DriverError),
200}
201
202impl MongoStoreError {
203 pub fn not_found(entity: impl Into<String>) -> Self {
204 Self::NotFound {
205 entity: entity.into(),
206 }
207 }
208
209 pub fn is_not_found(&self) -> bool {
210 matches!(self, Self::NotFound { .. })
211 }
212}
213
214impl fmt::Display for MongoStoreError {
215 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
216 match self {
217 Self::InvalidDatabase => formatter.write_str("MongoDB database name cannot be empty"),
218 Self::InvalidBatchSize => {
219 formatter.write_str("MongoDB bulk batch size must be positive")
220 }
221 Self::NotFound { entity } => write!(formatter, "{entity} not found"),
222 Self::Driver(error) => write!(formatter, "MongoDB operation failed: {error}"),
223 }
224 }
225}
226
227impl Error for MongoStoreError {
228 fn source(&self) -> Option<&(dyn Error + 'static)> {
229 match self {
230 Self::InvalidDatabase | Self::InvalidBatchSize | Self::NotFound { .. } => None,
231 Self::Driver(error) => Some(error),
232 }
233 }
234}
235
236impl From<DriverError> for MongoStoreError {
237 fn from(error: DriverError) -> Self {
238 Self::Driver(error)
239 }
240}
241
242#[derive(Clone)]
244pub struct MongoStore {
245 client: Client,
246 database: Database,
247 metrics: Option<MongoStoreMetrics>,
248}
249
250impl fmt::Debug for MongoStore {
251 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
252 formatter
253 .debug_struct("MongoStore")
254 .field("client", &self.client)
255 .field("database", &self.database)
256 .field("instrumented", &self.metrics.is_some())
257 .finish()
258 }
259}
260
261impl MongoStore {
262 pub async fn connect(config: MongoStoreConfig) -> Result<Self, MongoStoreError> {
263 if config.database.trim().is_empty() {
264 return Err(MongoStoreError::InvalidDatabase);
265 }
266
267 let mut options = ClientOptions::parse(&config.uri).await?;
268 options.app_name = config.application_name;
269 options.min_pool_size = config.min_pool_size;
270 options.max_pool_size = config.max_pool_size;
271 options.connect_timeout = config.connect_timeout;
272 options.server_selection_timeout = config.server_selection_timeout;
273 let client = Client::with_options(options)?;
274 let database = client.database(&config.database);
275 Ok(Self {
276 client,
277 database,
278 metrics: None,
279 })
280 }
281
282 pub fn from_client(client: Client, database: impl AsRef<str>) -> Result<Self, MongoStoreError> {
283 let database = database.as_ref();
284 if database.trim().is_empty() {
285 return Err(MongoStoreError::InvalidDatabase);
286 }
287 Ok(Self {
288 database: client.database(database),
289 client,
290 metrics: None,
291 })
292 }
293
294 pub fn with_metrics(mut self, metrics: MongoStoreMetrics) -> Self {
296 self.metrics = Some(metrics);
297 self
298 }
299
300 pub fn client(&self) -> &Client {
301 &self.client
302 }
303
304 pub fn database(&self) -> &Database {
305 &self.database
306 }
307
308 pub fn collection<T>(&self, name: impl AsRef<str>) -> Collection<T>
309 where
310 T: Send + Sync,
311 {
312 self.database.collection(name.as_ref())
313 }
314
315 pub fn cached_collection<K, T>(
316 &self,
317 name: impl AsRef<str>,
318 config: MongoCacheConfig,
319 ) -> CachedMongoCollection<K, T>
320 where
321 K: Clone + Eq + Hash,
322 T: Clone + Send + Sync,
323 {
324 CachedMongoCollection::new(self.collection(name), config)
325 }
326
327 pub fn cached_indexed_collection<K, I, T>(
329 &self,
330 name: impl AsRef<str>,
331 config: MongoCacheConfig,
332 ) -> CachedMongoCollection<K, T, I>
333 where
334 K: Clone + Eq + Hash,
335 I: Clone + Eq + Hash,
336 T: Clone + Send + Sync,
337 {
338 CachedMongoCollection::new(self.collection(name), config)
339 }
340
341 pub async fn health_check(&self) -> Result<(), MongoStoreError> {
342 self.database.run_command(doc! { "ping": 1 }).await?;
343 Ok(())
344 }
345
346 pub async fn query<T, R, F, Fut>(
348 &self,
349 operation: &'static str,
350 collection: impl AsRef<str>,
351 query: F,
352 ) -> Result<R, MongoStoreError>
353 where
354 T: Send + Sync,
355 F: FnOnce(Collection<T>) -> Fut,
356 Fut: Future<Output = Result<R, DriverError>>,
357 {
358 let collection = self.collection(collection);
359 self.instrument(operation, MongoOperationKind::Query, async move {
360 query(collection).await.map_err(MongoStoreError::Driver)
361 })
362 .await
363 }
364
365 pub async fn query_one<T, F, Fut>(
367 &self,
368 operation: &'static str,
369 collection: impl AsRef<str>,
370 entity: impl Into<String>,
371 query: F,
372 ) -> Result<T, MongoStoreError>
373 where
374 T: Send + Sync,
375 F: FnOnce(Collection<T>) -> Fut,
376 Fut: Future<Output = Result<Option<T>, DriverError>>,
377 {
378 let collection = self.collection(collection);
379 let entity = entity.into();
380 self.instrument(operation, MongoOperationKind::Query, async move {
381 query(collection)
382 .await
383 .map_err(MongoStoreError::Driver)?
384 .ok_or_else(|| MongoStoreError::not_found(entity))
385 })
386 .await
387 }
388
389 pub async fn execute<T, R, F, Fut>(
391 &self,
392 operation: &'static str,
393 collection: impl AsRef<str>,
394 execute: F,
395 ) -> Result<R, MongoStoreError>
396 where
397 T: Send + Sync,
398 F: FnOnce(Collection<T>) -> Fut,
399 Fut: Future<Output = Result<R, DriverError>>,
400 {
401 let collection = self.collection(collection);
402 self.instrument(operation, MongoOperationKind::Execute, async move {
403 execute(collection).await.map_err(MongoStoreError::Driver)
404 })
405 .await
406 }
407
408 pub async fn bulk_insert<T>(
410 &self,
411 operation: &'static str,
412 collection: impl AsRef<str>,
413 items: impl IntoIterator<Item = T>,
414 batch_size: usize,
415 ) -> Result<Vec<InsertManyResult>, MongoStoreError>
416 where
417 T: Serialize + Send + Sync,
418 {
419 if batch_size == 0 {
420 return Err(MongoStoreError::InvalidBatchSize);
421 }
422
423 let collection = self.collection::<T>(collection);
424 let mut items = items.into_iter();
425 self.instrument(operation, MongoOperationKind::BulkInsert, async move {
426 let mut results = Vec::new();
427 loop {
428 let batch: Vec<_> = items.by_ref().take(batch_size).collect();
429 if batch.is_empty() {
430 return Ok(results);
431 }
432 results.push(collection.insert_many(batch).await?);
433 }
434 })
435 .await
436 }
437
438 async fn instrument<R, Fut>(
439 &self,
440 operation: &'static str,
441 kind: MongoOperationKind,
442 future: Fut,
443 ) -> Result<R, MongoStoreError>
444 where
445 Fut: Future<Output = Result<R, MongoStoreError>>,
446 {
447 let started = Instant::now();
448 #[cfg(feature = "telemetry")]
449 let span = TelemetrySpan::start(
450 format!("mongo.{operation}"),
451 TelemetrySpanKind::Client,
452 None,
453 [
454 ("db.operation.name", operation.to_owned()),
455 ("rust_zero.mongo.kind", kind.as_str().to_owned()),
456 ],
457 );
458 let result = future.await;
459 let outcome = mongo_outcome(&result);
460 if let Some(metrics) = &self.metrics {
461 metrics.observe(operation, kind, outcome, started.elapsed());
462 }
463 #[cfg(feature = "telemetry")]
464 if let Err(error) = &result {
465 span.set_error(error.to_string());
466 }
467 result
468 }
469
470 pub async fn begin(&self) -> Result<ClientSession, MongoStoreError> {
472 let mut session = self.client.start_session().await?;
473 session.start_transaction().await?;
474 Ok(session)
475 }
476}
477
478fn mongo_outcome<T>(result: &Result<T, MongoStoreError>) -> &'static str {
479 match result {
480 Ok(_) => "success",
481 Err(MongoStoreError::NotFound { .. }) => "not_found",
482 Err(
483 MongoStoreError::InvalidDatabase
484 | MongoStoreError::InvalidBatchSize
485 | MongoStoreError::Driver(_),
486 ) => "error",
487 }
488}
489
490pub struct CachedMongoCollection<K, T, I = K>
492where
493 T: Send + Sync,
494{
495 collection: Collection<T>,
496 cache: MemoryCache<K, Option<T>>,
497 indexes: MemoryCache<I, Option<K>>,
498 flights: SingleFlight<K, Option<T>, DriverError>,
499 index_flights: SingleFlight<I, Option<(K, T)>, DriverError>,
500 ttl: Duration,
501 not_found_ttl: Option<Duration>,
502 ttl_jitter: Duration,
503 expiry_sequence: AtomicU64,
504 generation: AtomicU64,
505 cache_gate: Mutex<()>,
506}
507
508impl<K, T, I> CachedMongoCollection<K, T, I>
509where
510 K: Clone + Eq + Hash,
511 I: Clone + Eq + Hash,
512 T: Clone + Send + Sync,
513{
514 pub fn new(collection: Collection<T>, config: MongoCacheConfig) -> Self {
515 Self {
516 collection,
517 cache: MemoryCache::new(config.capacity),
518 indexes: MemoryCache::new(config.capacity),
519 flights: SingleFlight::new(),
520 index_flights: SingleFlight::new(),
521 ttl: config.ttl,
522 not_found_ttl: config.not_found_ttl,
523 ttl_jitter: config.ttl_jitter,
524 expiry_sequence: AtomicU64::new(0),
525 generation: AtomicU64::new(0),
526 cache_gate: Mutex::new(()),
527 }
528 }
529
530 pub fn collection(&self) -> &Collection<T> {
531 &self.collection
532 }
533
534 pub async fn find<F, Fut>(
535 &self,
536 key: K,
537 query: F,
538 ) -> Result<Option<T>, SingleFlightError<DriverError>>
539 where
540 F: FnOnce(Collection<T>) -> Fut,
541 Fut: Future<Output = Result<Option<T>, DriverError>>,
542 {
543 if let Some(value) = self.cache.get(&key) {
544 return Ok(value);
545 }
546
547 self.flights
548 .execute(key.clone(), || async {
549 if let Some(value) = self.cache.get(&key) {
550 return Ok(value);
551 }
552 let generation = self.generation.load(Ordering::Acquire);
553 let value = query(self.collection.clone()).await?;
554 let _guard = self.cache_gate.lock().expect("MongoDB cache gate poisoned");
555 if self.generation.load(Ordering::Acquire) == generation {
556 let base_ttl = if value.is_some() {
557 Some(self.ttl)
558 } else {
559 self.not_found_ttl
560 };
561 if let Some(base_ttl) = base_ttl {
562 let sequence = self.expiry_sequence.fetch_add(1, Ordering::Relaxed);
563 self.cache.insert(
564 key,
565 value.clone(),
566 jittered_ttl(base_ttl, self.ttl_jitter, sequence),
567 );
568 }
569 }
570 Ok(value)
571 })
572 .await
573 }
574
575 pub async fn find_by_index<F, Fut>(
577 &self,
578 index: I,
579 query: F,
580 ) -> Result<Option<T>, SingleFlightError<DriverError>>
581 where
582 F: FnOnce(Collection<T>) -> Fut,
583 Fut: Future<Output = Result<Option<(K, T)>, DriverError>>,
584 {
585 if let Some(primary) = self.indexes.get(&index) {
586 match primary {
587 Some(primary) => {
588 if let Some(value) = self.cache.get(&primary) {
589 return Ok(value);
590 }
591 }
592 None => return Ok(None),
593 }
594 }
595
596 let loaded = self
597 .index_flights
598 .execute(index.clone(), || async {
599 if let Some(primary) = self.indexes.get(&index) {
600 match primary {
601 Some(primary) => {
602 if let Some(Some(value)) = self.cache.get(&primary) {
603 return Ok(Some((primary, value)));
604 }
605 }
606 None => return Ok(None),
607 }
608 }
609
610 let generation = self.generation.load(Ordering::Acquire);
611 let value = query(self.collection.clone()).await?;
612 let _guard = self.cache_gate.lock().expect("MongoDB cache gate poisoned");
613 if self.generation.load(Ordering::Acquire) == generation {
614 let base_ttl = if value.is_some() {
615 Some(self.ttl)
616 } else {
617 self.not_found_ttl
618 };
619 if let Some(base_ttl) = base_ttl {
620 let sequence = self.expiry_sequence.fetch_add(1, Ordering::Relaxed);
621 let ttl = jittered_ttl(base_ttl, self.ttl_jitter, sequence);
622 let primary = value.as_ref().map(|(primary, _)| primary.clone());
623 self.indexes.insert(index, primary, ttl);
624 if let Some((primary, value)) = &value {
625 self.cache.insert(primary.clone(), Some(value.clone()), ttl);
626 }
627 }
628 }
629 Ok(value)
630 })
631 .await?;
632 Ok(loaded.map(|(_, value)| value))
633 }
634
635 pub async fn execute<PI, F, Fut, R>(&self, keys: PI, operation: F) -> Result<R, DriverError>
636 where
637 PI: IntoIterator<Item = K>,
638 F: FnOnce(Collection<T>) -> Fut,
639 Fut: Future<Output = Result<R, DriverError>>,
640 {
641 let result = operation(self.collection.clone()).await?;
642 self.invalidate_many(keys);
643 Ok(result)
644 }
645
646 pub async fn execute_indexed<PI, SI, F, Fut, R>(
651 &self,
652 primary_keys: PI,
653 index_keys: SI,
654 operation: F,
655 ) -> Result<R, DriverError>
656 where
657 PI: IntoIterator<Item = K>,
658 SI: IntoIterator<Item = I>,
659 F: FnOnce(Collection<T>) -> Fut,
660 Fut: Future<Output = Result<R, DriverError>>,
661 {
662 let result = operation(self.collection.clone()).await?;
663 self.invalidate_related(primary_keys, index_keys);
664 Ok(result)
665 }
666
667 pub async fn insert_one<SI>(
669 &self,
670 primary_key: K,
671 index_keys: SI,
672 document: T,
673 ) -> Result<InsertOneResult, DriverError>
674 where
675 SI: IntoIterator<Item = I>,
676 T: Serialize,
677 {
678 self.execute_indexed([primary_key], index_keys, move |collection| async move {
679 collection.insert_one(document).await
680 })
681 .await
682 }
683
684 pub async fn insert_many<PI, SI>(
686 &self,
687 primary_keys: PI,
688 index_keys: SI,
689 documents: Vec<T>,
690 ) -> Result<InsertManyResult, DriverError>
691 where
692 PI: IntoIterator<Item = K>,
693 SI: IntoIterator<Item = I>,
694 T: Serialize,
695 {
696 self.execute_indexed(primary_keys, index_keys, move |collection| async move {
697 collection.insert_many(documents).await
698 })
699 .await
700 }
701
702 pub async fn update_one<PI, SI>(
704 &self,
705 primary_keys: PI,
706 index_keys: SI,
707 filter: Document,
708 update: Document,
709 ) -> Result<UpdateResult, DriverError>
710 where
711 PI: IntoIterator<Item = K>,
712 SI: IntoIterator<Item = I>,
713 {
714 self.execute_indexed(primary_keys, index_keys, move |collection| async move {
715 collection.update_one(filter, update).await
716 })
717 .await
718 }
719
720 pub async fn replace_one<PI, SI>(
722 &self,
723 primary_keys: PI,
724 index_keys: SI,
725 filter: Document,
726 replacement: T,
727 ) -> Result<UpdateResult, DriverError>
728 where
729 PI: IntoIterator<Item = K>,
730 SI: IntoIterator<Item = I>,
731 T: Serialize,
732 {
733 self.execute_indexed(primary_keys, index_keys, move |collection| async move {
734 collection.replace_one(filter, replacement).await
735 })
736 .await
737 }
738
739 pub async fn delete_one<PI, SI>(
741 &self,
742 primary_keys: PI,
743 index_keys: SI,
744 filter: Document,
745 ) -> Result<DeleteResult, DriverError>
746 where
747 PI: IntoIterator<Item = K>,
748 SI: IntoIterator<Item = I>,
749 {
750 self.execute_indexed(primary_keys, index_keys, move |collection| async move {
751 collection.delete_one(filter).await
752 })
753 .await
754 }
755
756 pub fn invalidate(&self, key: &K) -> bool {
757 let _guard = self.cache_gate.lock().expect("MongoDB cache gate poisoned");
758 self.generation.fetch_add(1, Ordering::AcqRel);
759 let removed = self.cache.remove(key).is_some();
760 self.indexes
761 .remove_where(|_, primary| primary.as_ref() == Some(key));
762 removed
763 }
764
765 pub fn invalidate_many<PI>(&self, keys: PI)
766 where
767 PI: IntoIterator<Item = K>,
768 {
769 let _guard = self.cache_gate.lock().expect("MongoDB cache gate poisoned");
770 self.generation.fetch_add(1, Ordering::AcqRel);
771 let keys: std::collections::HashSet<_> = keys.into_iter().collect();
772 for key in &keys {
773 self.cache.remove(key);
774 }
775 self.indexes
776 .remove_where(|_, primary| primary.as_ref().is_some_and(|key| keys.contains(key)));
777 }
778
779 pub fn invalidate_index(&self, index: &I) -> bool {
780 let _guard = self.cache_gate.lock().expect("MongoDB cache gate poisoned");
781 self.generation.fetch_add(1, Ordering::AcqRel);
782 self.indexes.remove(index).is_some()
783 }
784
785 pub fn invalidate_related<PI, SI>(&self, primary_keys: PI, index_keys: SI)
786 where
787 PI: IntoIterator<Item = K>,
788 SI: IntoIterator<Item = I>,
789 {
790 let _guard = self.cache_gate.lock().expect("MongoDB cache gate poisoned");
791 self.generation.fetch_add(1, Ordering::AcqRel);
792 let primary_keys: std::collections::HashSet<_> = primary_keys.into_iter().collect();
793 for key in &primary_keys {
794 self.cache.remove(key);
795 }
796 self.indexes.remove_where(|_, primary| {
797 primary
798 .as_ref()
799 .is_some_and(|key| primary_keys.contains(key))
800 });
801 for index in index_keys {
802 self.indexes.remove(&index);
803 }
804 }
805
806 pub fn cache_stats(&self) -> CacheStats {
807 self.cache.stats()
808 }
809
810 pub fn index_cache_stats(&self) -> CacheStats {
811 self.indexes.stats()
812 }
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818 use serde::{Deserialize, Serialize};
819 use std::sync::atomic::{AtomicUsize, Ordering};
820
821 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
822 struct User {
823 #[serde(rename = "_id")]
824 id: i64,
825 name: String,
826 }
827
828 #[test]
829 fn cache_policy_supports_jitter_and_optional_negative_entries() {
830 let config = MongoCacheConfig::new(128, Duration::from_secs(30))
831 .with_not_found_ttl(None)
832 .with_ttl_jitter(Duration::from_secs(5));
833
834 assert_eq!(config.not_found_ttl, None);
835 assert_eq!(config.ttl_jitter, Duration::from_secs(5));
836 }
837
838 #[tokio::test]
839 async fn validates_configuration_and_caches_positive_and_negative_results() {
840 let error = MongoStore::connect(MongoStoreConfig::new("mongodb://127.0.0.1:27017", " "))
841 .await
842 .unwrap_err();
843 assert!(matches!(error, MongoStoreError::InvalidDatabase));
844
845 let store = MongoStore::connect(MongoStoreConfig::new(
846 "mongodb://127.0.0.1:27017",
847 "rust_zero_test",
848 ))
849 .await
850 .unwrap();
851 let cached = store.cached_collection::<i64, User>(
852 "users",
853 MongoCacheConfig::new(10, Duration::from_secs(30)),
854 );
855 let queries = AtomicUsize::new(0);
856
857 for _ in 0..2 {
858 let queries = &queries;
859 let user = cached
860 .find(7, move |_collection| async move {
861 queries.fetch_add(1, Ordering::SeqCst);
862 Ok(Some(User {
863 id: 7,
864 name: "Ada".to_owned(),
865 }))
866 })
867 .await
868 .unwrap();
869 assert_eq!(user.unwrap().name, "Ada");
870 }
871 for _ in 0..2 {
872 let queries = &queries;
873 assert!(cached
874 .find(404, move |_collection| async move {
875 queries.fetch_add(1, Ordering::SeqCst);
876 Ok(None)
877 })
878 .await
879 .unwrap()
880 .is_none());
881 }
882 assert_eq!(queries.load(Ordering::SeqCst), 2);
883 assert!(cached.invalidate(&7));
884 }
885
886 #[tokio::test]
887 async fn typed_helpers_standardize_not_found_validate_batches_and_emit_metrics() {
888 let registry = Metrics::new();
889 let metrics = MongoStoreMetrics::register(®istry).unwrap();
890 let store = MongoStore::connect(MongoStoreConfig::new(
891 "mongodb://127.0.0.1:27017",
892 "rust_zero_test",
893 ))
894 .await
895 .unwrap()
896 .with_metrics(metrics);
897
898 let user = store
899 .query::<User, _, _, _>("find_user", "users", |_collection| async {
900 Ok(User {
901 id: 7,
902 name: "Ada".to_owned(),
903 })
904 })
905 .await
906 .unwrap();
907 assert_eq!(user.name, "Ada");
908
909 let missing = store
910 .query_one::<User, _, _>("find_user", "users", "user", |_collection| async {
911 Ok(None)
912 })
913 .await
914 .unwrap_err();
915 assert!(missing.is_not_found());
916 assert_eq!(missing.to_string(), "user not found");
917
918 assert!(matches!(
919 store.bulk_insert::<User>("invalid", "users", [], 0).await,
920 Err(MongoStoreError::InvalidBatchSize)
921 ));
922 let rendered = registry.render();
923 assert!(rendered.contains(
924 "rust_zero_mongo_operations_total{operation=\"find_user\",kind=\"query\",outcome=\"success\"} 1"
925 ));
926 assert!(rendered.contains(
927 "rust_zero_mongo_operations_total{operation=\"find_user\",kind=\"query\",outcome=\"not_found\"} 1"
928 ));
929 }
930
931 #[tokio::test]
932 async fn secondary_indexes_share_documents_and_follow_primary_invalidation() {
933 let store = MongoStore::connect(MongoStoreConfig::new(
934 "mongodb://127.0.0.1:27017",
935 "rust_zero_test",
936 ))
937 .await
938 .unwrap();
939 let cached = store.cached_indexed_collection::<i64, String, User>(
940 "users",
941 MongoCacheConfig::new(10, Duration::from_secs(30)),
942 );
943 let queries = AtomicUsize::new(0);
944
945 for _ in 0..2 {
946 let queries = &queries;
947 let user = cached
948 .find_by_index("ada@test".to_owned(), move |_collection| async move {
949 queries.fetch_add(1, Ordering::SeqCst);
950 Ok(Some((
951 7,
952 User {
953 id: 7,
954 name: "Ada".to_owned(),
955 },
956 )))
957 })
958 .await
959 .unwrap();
960 assert_eq!(user.unwrap().name, "Ada");
961 }
962 assert_eq!(queries.load(Ordering::SeqCst), 1);
963
964 assert!(cached.invalidate(&7));
965 let queries = &queries;
966 let user = cached
967 .find_by_index("ada@test".to_owned(), move |_collection| async move {
968 queries.fetch_add(1, Ordering::SeqCst);
969 Ok(Some((
970 7,
971 User {
972 id: 7,
973 name: "Grace".to_owned(),
974 },
975 )))
976 })
977 .await
978 .unwrap();
979 assert_eq!(user.unwrap().name, "Grace");
980 assert_eq!(queries.load(Ordering::SeqCst), 2);
981 assert!(cached.index_cache_stats().insertions >= 2);
982 }
983
984 #[tokio::test]
985 async fn mongodb_integration_covers_health_crud_cache_and_transactions() {
986 let Ok(uri) = std::env::var("RUST_ZERO_MONGODB_URI") else {
987 return;
988 };
989 let database = format!("rust_zero_{}", std::process::id());
990 let store = MongoStore::connect(MongoStoreConfig::new(uri, &database))
991 .await
992 .unwrap();
993 store.health_check().await.unwrap();
994 let users = store.cached_collection::<i64, User>(
995 "users",
996 MongoCacheConfig::new(100, Duration::from_secs(30)),
997 );
998 users
999 .insert_one(
1000 7,
1001 std::iter::empty(),
1002 User {
1003 id: 7,
1004 name: "Ada".to_owned(),
1005 },
1006 )
1007 .await
1008 .unwrap();
1009 let user = users
1010 .find(7, |collection| async move {
1011 collection.find_one(doc! { "_id": 7_i64 }).await
1012 })
1013 .await
1014 .unwrap()
1015 .unwrap();
1016 assert_eq!(user.name, "Ada");
1017 users
1018 .update_one(
1019 [7],
1020 std::iter::empty(),
1021 doc! { "_id": 7_i64 },
1022 doc! { "$set": { "name": "Grace" } },
1023 )
1024 .await
1025 .unwrap();
1026 let user = users
1027 .find(7, |collection| async move {
1028 collection.find_one(doc! { "_id": 7_i64 }).await
1029 })
1030 .await
1031 .unwrap()
1032 .unwrap();
1033 assert_eq!(user.name, "Grace");
1034
1035 let bulk = store
1036 .bulk_insert(
1037 "insert_users",
1038 "users",
1039 (8_i64..=10).map(|id| User {
1040 id,
1041 name: format!("user-{id}"),
1042 }),
1043 2,
1044 )
1045 .await
1046 .unwrap();
1047 assert_eq!(bulk.len(), 2);
1048 assert_eq!(
1049 bulk.iter()
1050 .map(|result| result.inserted_ids.len())
1051 .sum::<usize>(),
1052 3
1053 );
1054
1055 if std::env::var_os("RUST_ZERO_MONGODB_TRANSACTIONS").is_some() {
1056 let mut transaction = store.begin().await.unwrap();
1057 users
1058 .collection()
1059 .delete_one(doc! { "_id": 7_i64 })
1060 .session(&mut transaction)
1061 .await
1062 .unwrap();
1063 transaction.abort_transaction().await.unwrap();
1064 }
1065 store.database().drop().await.unwrap();
1066 }
1067}