rightsize_modules/
mongodb.rs1use std::time::Duration;
7
8use rightsize::{BoxFuture, Container, ContainerGuard, Result, RightsizeError, Wait};
9
10const REPLICA_SET_TIMEOUT: Duration = Duration::from_secs(60);
11const POLL_INTERVAL: Duration = Duration::from_millis(500);
12
13pub struct MongoDbContainer(Container);
16
17impl MongoDbContainer {
18 const PORT: u16 = 27017;
20
21 pub fn new() -> Self {
23 Self::with_image("mongo:8.0")
24 }
25
26 pub fn with_image(image: &str) -> Self {
28 let container = Container::new(image)
29 .with_exposed_ports(&[Self::PORT])
30 .with_command(&["mongod", "--replSet", "docker-rs", "--bind_ip_all"])
31 .waiting_for(Wait::for_listening_port())
32 .with_post_start(|guard: &ContainerGuard| -> BoxFuture<'_, Result<()>> {
33 Box::pin(async move {
34 initiate_replica_set(guard).await?;
35 await_primary_elected(guard).await
36 })
37 });
38 Self(container)
39 }
40
41 pub async fn start(self) -> Result<MongoDbGuard> {
43 crate::register_default_backends();
44 Ok(MongoDbGuard(self.0.start().await?))
45 }
46}
47
48impl Default for MongoDbContainer {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54async fn initiate_replica_set(guard: &ContainerGuard) -> Result<()> {
60 poll_until(guard, "rs.initiate to succeed", |guard| {
61 Box::pin(async move {
62 let result = guard
63 .exec(&[
64 "mongosh",
65 "--quiet",
66 "--eval",
67 "try { rs.status() } catch (e) { rs.initiate() }",
68 ])
69 .await;
70 matches!(result, Ok(r) if r.exit_code == 0)
71 })
72 })
73 .await
74}
75
76async fn await_primary_elected(guard: &ContainerGuard) -> Result<()> {
78 poll_until(guard, "a PRIMARY to be elected", |guard| {
79 Box::pin(async move {
80 let result = guard
81 .exec(&[
82 "mongosh",
83 "--quiet",
84 "--eval",
85 "db.hello().isWritablePrimary",
86 ])
87 .await;
88 matches!(result, Ok(r) if r.stdout.trim().ends_with("true"))
89 })
90 })
91 .await
92}
93
94async fn poll_until<F>(guard: &ContainerGuard, what: &str, mut cond: F) -> Result<()>
98where
99 F: FnMut(&ContainerGuard) -> BoxFuture<'_, bool>,
100{
101 let deadline = tokio::time::Instant::now() + REPLICA_SET_TIMEOUT;
102 loop {
103 if cond(guard).await {
104 return Ok(());
105 }
106 if tokio::time::Instant::now() >= deadline {
107 break;
108 }
109 tokio::time::sleep(POLL_INTERVAL).await;
110 }
111 Err(RightsizeError::ContainerLaunch(format!(
112 "Mongo replica set on {}:{} did not reach '{what}' within {}s",
113 guard.host(),
114 guard.get_mapped_port(MongoDbContainer::PORT).unwrap_or(0),
115 REPLICA_SET_TIMEOUT.as_secs(),
116 )))
117}
118
119pub struct MongoDbGuard(ContainerGuard);
121
122impl MongoDbGuard {
123 pub fn connection_string(&self) -> String {
125 format!(
126 "mongodb://{}:{}/test?directConnection=true",
127 self.0.host(),
128 self.0.get_mapped_port(MongoDbContainer::PORT).unwrap()
129 )
130 }
131
132 pub fn replica_set_url(&self) -> String {
135 self.connection_string()
136 }
137
138 pub async fn stop(self) -> Result<()> {
140 self.0.stop().await
141 }
142}
143
144impl std::ops::Deref for MongoDbGuard {
145 type Target = ContainerGuard;
146 fn deref(&self) -> &ContainerGuard {
147 &self.0
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn with_image_smoke() {
157 let _ = MongoDbContainer::new();
158 let _ = MongoDbContainer::with_image("mongo:8.0");
159 }
160}