Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: preliminary refactoring of wait strategies #661

Merged
merged 3 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 4 additions & 45 deletions testcontainers/src/core/containers/async_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
core::{
client::Client,
env,
error::{ContainerMissingInfo, ExecError, Result, TestcontainersError, WaitContainerError},
error::{ContainerMissingInfo, ExecError, Result, TestcontainersError},
macros,
network::Network,
ports::Ports,
Expand Down Expand Up @@ -303,50 +303,9 @@ where
let id = self.id();

for condition in ready_conditions {
match condition {
WaitFor::StdOutMessage { message } => self
.docker_client
.stdout_logs(id, true)
.wait_for_message(message)
.await
.map_err(WaitContainerError::from)?,
WaitFor::StdErrMessage { message } => self
.docker_client
.stderr_logs(id, true)
.wait_for_message(message)
.await
.map_err(WaitContainerError::from)?,
WaitFor::Duration { length } => {
tokio::time::sleep(length).await;
}
WaitFor::Healthcheck => loop {
use bollard::models::HealthStatusEnum::*;

let health_status = self
.docker_client
.inspect(id)
.await?
.state
.ok_or(WaitContainerError::StateUnavailable)?
.health
.and_then(|health| health.status);

match health_status {
Some(HEALTHY) => break,
None | Some(EMPTY) | Some(NONE) => {
Err(WaitContainerError::HealthCheckNotConfigured(id.to_string()))?
}
Some(UNHEALTHY) => Err(WaitContainerError::Unhealthy)?,
Some(STARTING) => {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
},
WaitFor::Http(http_strategy) => {
http_strategy.wait_until_ready(self).await?;
}
WaitFor::Nothing => {}
}
condition
.wait_until_ready(&self.docker_client, self)
.await?;
}

log::debug!("Container {id} is now ready!");
Expand Down
64 changes: 64 additions & 0 deletions testcontainers/src/core/wait/health_strategy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::time::Duration;

use bollard::models::HealthStatusEnum::*;

use crate::{
core::{client::Client, error::WaitContainerError, wait::WaitStrategy},
ContainerAsync, Image,
};

#[derive(Debug, Clone)]
pub struct HealthWaitStrategy {
poll_interval: Duration,
}

impl HealthWaitStrategy {
/// Create a new `HealthWaitStrategy` with default settings.
pub fn new() -> Self {
Self {
poll_interval: Duration::from_millis(100),
}
}

/// Set the poll interval for checking the container's health status.
pub fn with_poll_interval(mut self, poll_interval: Duration) -> Self {
self.poll_interval = poll_interval;
self
}
}

impl WaitStrategy for HealthWaitStrategy {
async fn wait_until_ready<I: Image>(
self,
client: &Client,
container: &ContainerAsync<I>,
) -> crate::core::error::Result<()> {
loop {
let health_status = client
.inspect(container.id())
.await?
.state
.ok_or(WaitContainerError::StateUnavailable)?
.health
.and_then(|health| health.status);

match health_status {
Some(HEALTHY) => break,
None | Some(EMPTY) | Some(NONE) => Err(
WaitContainerError::HealthCheckNotConfigured(container.id().to_string()),
)?,
Some(UNHEALTHY) => Err(WaitContainerError::Unhealthy)?,
Some(STARTING) => {
tokio::time::sleep(self.poll_interval).await;
}
}
}
Ok(())
}
}

impl Default for HealthWaitStrategy {
fn default() -> Self {
Self::new()
}
}
3 changes: 2 additions & 1 deletion testcontainers/src/core/wait/http_strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use bytes::Bytes;
use url::{Host, Url};

use crate::{
core::{error::WaitContainerError, wait::WaitStrategy, ContainerPort},
core::{client::Client, error::WaitContainerError, wait::WaitStrategy, ContainerPort},
ContainerAsync, Image, TestcontainersError,
};

Expand Down Expand Up @@ -205,6 +205,7 @@ impl HttpWaitStrategy {
impl WaitStrategy for HttpWaitStrategy {
async fn wait_until_ready<I: Image>(
self,
_client: &Client,
container: &ContainerAsync<I>,
) -> crate::core::error::Result<()> {
let host = container.get_host().await?;
Expand Down
51 changes: 47 additions & 4 deletions testcontainers/src/core/wait/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
use std::{env::var, fmt::Debug, time::Duration};

use bytes::Bytes;
pub use health_strategy::HealthWaitStrategy;
pub use http_strategy::HttpWaitStrategy;

use crate::Image;
use crate::{
core::{client::Client, error::WaitContainerError},
ContainerAsync, Image,
};

pub(crate) mod cmd_wait;
pub(crate) mod health_strategy;
pub(crate) mod http_strategy;

pub(crate) trait WaitStrategy {
async fn wait_until_ready<I: Image>(
self,
container: &crate::ContainerAsync<I>,
client: &Client,
container: &ContainerAsync<I>,
) -> crate::core::error::Result<()>;
}

Expand All @@ -27,7 +33,7 @@ pub enum WaitFor {
/// Wait for a certain amount of time.
Duration { length: Duration },
/// Wait for the container's status to become `healthy`.
Healthcheck,
Healthcheck(HealthWaitStrategy),
/// Wait for a certain HTTP response.
Http(HttpWaitStrategy),
}
Expand All @@ -48,8 +54,11 @@ impl WaitFor {
}

/// Wait for the container to become healthy.
///
/// If you need to customize polling interval, use [`HealthWaitStrategy::with_poll_interval`]
/// and create the strategy [`WaitFor::Healthcheck`] manually.
pub fn healthcheck() -> WaitFor {
WaitFor::Healthcheck
WaitFor::Healthcheck(HealthWaitStrategy::default())
}

/// Wait for a certain HTTP response.
Expand Down Expand Up @@ -97,3 +106,37 @@ impl From<HttpWaitStrategy> for WaitFor {
Self::Http(value)
}
}

impl WaitStrategy for WaitFor {
async fn wait_until_ready<I: Image>(
self,
client: &Client,
container: &ContainerAsync<I>,
) -> crate::core::error::Result<()> {
match self {
// TODO: introduce log-followers feature and switch to it (wait for signal from follower).
// Thus, avoiding multiple log requests.
WaitFor::StdOutMessage { message } => client
.stdout_logs(container.id(), true)
.wait_for_message(message)
.await
.map_err(WaitContainerError::from)?,
WaitFor::StdErrMessage { message } => client
.stderr_logs(container.id(), true)
.wait_for_message(message)
.await
.map_err(WaitContainerError::from)?,
WaitFor::Duration { length } => {
tokio::time::sleep(length).await;
}
WaitFor::Healthcheck(strategy) => {
strategy.wait_until_ready(client, container).await?;
}
WaitFor::Http(strategy) => {
strategy.wait_until_ready(client, container).await?;
}
WaitFor::Nothing => {}
}
Ok(())
}
}
Loading