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 Ok(MongoDbGuard(self.0.start().await?))
44 }
45}
46
47impl Default for MongoDbContainer {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53async fn initiate_replica_set(guard: &ContainerGuard) -> Result<()> {
59 poll_until(guard, "rs.initiate to succeed", |guard| {
60 Box::pin(async move {
61 let result = guard
62 .exec(&[
63 "mongosh",
64 "--quiet",
65 "--eval",
66 "try { rs.status() } catch (e) { rs.initiate() }",
67 ])
68 .await;
69 matches!(result, Ok(r) if r.exit_code == 0)
70 })
71 })
72 .await
73}
74
75async fn await_primary_elected(guard: &ContainerGuard) -> Result<()> {
77 poll_until(guard, "a PRIMARY to be elected", |guard| {
78 Box::pin(async move {
79 let result = guard
80 .exec(&[
81 "mongosh",
82 "--quiet",
83 "--eval",
84 "db.hello().isWritablePrimary",
85 ])
86 .await;
87 matches!(result, Ok(r) if r.stdout.trim().ends_with("true"))
88 })
89 })
90 .await
91}
92
93async fn poll_until<F>(guard: &ContainerGuard, what: &str, mut cond: F) -> Result<()>
97where
98 F: FnMut(&ContainerGuard) -> BoxFuture<'_, bool>,
99{
100 let deadline = tokio::time::Instant::now() + REPLICA_SET_TIMEOUT;
101 loop {
102 if cond(guard).await {
103 return Ok(());
104 }
105 if tokio::time::Instant::now() >= deadline {
106 break;
107 }
108 tokio::time::sleep(POLL_INTERVAL).await;
109 }
110 Err(RightsizeError::ContainerLaunch(format!(
111 "Mongo replica set on {}:{} did not reach '{what}' within {}s",
112 guard.host(),
113 guard.get_mapped_port(MongoDbContainer::PORT).unwrap_or(0),
114 REPLICA_SET_TIMEOUT.as_secs(),
115 )))
116}
117
118pub struct MongoDbGuard(ContainerGuard);
120
121impl MongoDbGuard {
122 pub fn connection_string(&self) -> String {
124 format!(
125 "mongodb://{}:{}/test?directConnection=true",
126 self.0.host(),
127 self.0.get_mapped_port(MongoDbContainer::PORT).unwrap()
128 )
129 }
130
131 pub fn replica_set_url(&self) -> String {
134 self.connection_string()
135 }
136
137 pub async fn stop(self) -> Result<()> {
139 self.0.stop().await
140 }
141}
142
143impl std::ops::Deref for MongoDbGuard {
144 type Target = ContainerGuard;
145 fn deref(&self) -> &ContainerGuard {
146 &self.0
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn with_image_smoke() {
156 let _ = MongoDbContainer::new();
157 let _ = MongoDbContainer::with_image("mongo:8.0");
158 }
159}