use std::time::Duration;
use rightsize::{BoxFuture, Container, ContainerGuard, Result, RightsizeError, Wait};
const REPLICA_SET_TIMEOUT: Duration = Duration::from_secs(60);
const POLL_INTERVAL: Duration = Duration::from_millis(500);
pub struct MongoDbContainer(Container);
impl MongoDbContainer {
const PORT: u16 = 27017;
pub fn new() -> Self {
Self::with_image("mongo:8.0")
}
pub fn with_image(image: &str) -> Self {
let container = Container::new(image)
.with_exposed_ports(&[Self::PORT])
.with_command(&["mongod", "--replSet", "docker-rs", "--bind_ip_all"])
.waiting_for(Wait::for_listening_port())
.with_post_start(|guard: &ContainerGuard| -> BoxFuture<'_, Result<()>> {
Box::pin(async move {
initiate_replica_set(guard).await?;
await_primary_elected(guard).await
})
});
Self(container)
}
pub async fn start(self) -> Result<MongoDbGuard> {
crate::register_default_backends();
Ok(MongoDbGuard(self.0.start().await?))
}
}
impl Default for MongoDbContainer {
fn default() -> Self {
Self::new()
}
}
async fn initiate_replica_set(guard: &ContainerGuard) -> Result<()> {
poll_until(guard, "rs.initiate to succeed", |guard| {
Box::pin(async move {
let result = guard
.exec(&[
"mongosh",
"--quiet",
"--eval",
"try { rs.status() } catch (e) { rs.initiate() }",
])
.await;
matches!(result, Ok(r) if r.exit_code == 0)
})
})
.await
}
async fn await_primary_elected(guard: &ContainerGuard) -> Result<()> {
poll_until(guard, "a PRIMARY to be elected", |guard| {
Box::pin(async move {
let result = guard
.exec(&[
"mongosh",
"--quiet",
"--eval",
"db.hello().isWritablePrimary",
])
.await;
matches!(result, Ok(r) if r.stdout.trim().ends_with("true"))
})
})
.await
}
async fn poll_until<F>(guard: &ContainerGuard, what: &str, mut cond: F) -> Result<()>
where
F: FnMut(&ContainerGuard) -> BoxFuture<'_, bool>,
{
let deadline = tokio::time::Instant::now() + REPLICA_SET_TIMEOUT;
loop {
if cond(guard).await {
return Ok(());
}
if tokio::time::Instant::now() >= deadline {
break;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
Err(RightsizeError::ContainerLaunch(format!(
"Mongo replica set on {}:{} did not reach '{what}' within {}s",
guard.host(),
guard.get_mapped_port(MongoDbContainer::PORT).unwrap_or(0),
REPLICA_SET_TIMEOUT.as_secs(),
)))
}
pub struct MongoDbGuard(ContainerGuard);
impl MongoDbGuard {
pub fn connection_string(&self) -> String {
format!(
"mongodb://{}:{}/test?directConnection=true",
self.0.host(),
self.0.get_mapped_port(MongoDbContainer::PORT).unwrap()
)
}
pub fn replica_set_url(&self) -> String {
self.connection_string()
}
pub async fn stop(self) -> Result<()> {
self.0.stop().await
}
}
impl std::ops::Deref for MongoDbGuard {
type Target = ContainerGuard;
fn deref(&self) -> &ContainerGuard {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_image_smoke() {
let _ = MongoDbContainer::new();
let _ = MongoDbContainer::with_image("mongo:8.0");
}
}