Skip to content

Commit

Permalink
Replace wait_until_ready with declarative ready_conditions
Browse files Browse the repository at this point in the history
Making this feature declarative has several advantages:

- It reduces the amount of code one needs to write for a new image.
- It avoids the need for an `ImageAsync` trait once we introduce that,
see discussions in #216 for details.
- It allows for a re-usable abstraction of waiting for a specific
amount of time, which fixes #39.
  • Loading branch information
thomaseizinger committed Sep 2, 2021
1 parent 9d6e86c commit 82ebb99
Show file tree
Hide file tree
Showing 18 changed files with 148 additions and 171 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- How images express when a container is ready: Instead of implementing `wait_until_ready`, images now need to implement `ready_conditions` which returns a list of `WaitFor` instances.

### Removed

- `DYNAMODB_ADDITIONAL_SLEEP_PERIOD` variable from `dynamodb_local` image.
Previously, we had a fallback of 2 seconds if this variable was not defined.
We now wait for 2 seconds unconditionally after the specified message has been found.

## [0.12.0] - 2021-01-27

### Added
Expand Down
6 changes: 4 additions & 2 deletions src/clients/cli.rs
Expand Up @@ -326,7 +326,7 @@ impl Ports {

#[cfg(test)]
mod tests {
use crate::{images::generic::GenericImage, Container, Docker, Image};
use crate::{core::WaitFor, images::generic::GenericImage, Docker, Image};

use super::*;

Expand Down Expand Up @@ -397,7 +397,9 @@ mod tests {
String::from("hello-world")
}

fn wait_until_ready<D: Docker>(&self, _container: &Container<'_, D, Self>) {}
fn ready_conditions(&self) -> Vec<WaitFor> {
vec![]
}

fn args(&self) -> <Self as Image>::Args {
vec![]
Expand Down
2 changes: 1 addition & 1 deletion src/core.rs
@@ -1,7 +1,7 @@
pub use self::{
container::Container,
docker::{Docker, Logs, Ports, RunArgs},
image::{Image, Port},
image::{Image, Port, WaitFor},
wait_for_message::{WaitError, WaitForMessage},
};

Expand Down
20 changes: 18 additions & 2 deletions src/core/container.rs
@@ -1,4 +1,7 @@
use crate::{core::Logs, Docker, Image};
use crate::{
core::{image::WaitFor, Logs},
Docker, Image, WaitForMessage,
};
use std::env::var;

/// Represents a running docker container.
Expand Down Expand Up @@ -110,7 +113,20 @@ where
fn block_until_ready(&self) {
log::debug!("Waiting for container {} to be ready", self.id);

self.image.wait_until_ready(self);
for condition in self.image.ready_conditions() {
match condition {
WaitFor::StdOutMessage { message } => {
self.logs().stdout.wait_for_message(&message).unwrap()
}
WaitFor::StdErrMessage { message } => {
self.logs().stderr.wait_for_message(&message).unwrap()
}
WaitFor::Duration { length } => {
std::thread::sleep(length);
}
WaitFor::Nothing => {}
}
}

log::debug!("Container {} is now ready!", self.id);
}
Expand Down
67 changes: 59 additions & 8 deletions src/core/image.rs
@@ -1,4 +1,4 @@
use crate::core::{Container, Docker};
use std::{env::var, time::Duration};

/// Represents a docker image.
///
Expand Down Expand Up @@ -63,18 +63,17 @@ where
/// suddenly changed.
fn descriptor(&self) -> String;

/// Blocks the current thread until the started container is ready.
/// Returns a list of conditions that need to be met before a started container is considered ready.
///
/// This method is the **🍞 and butter** of the whole testcontainers library. Containers are
/// rarely instantly available as soon as they are started. Most of them take some time to boot
/// up.
///
/// Implementations MUST block the current thread until the passed-in container is ready to be
/// interacted with. The container instance provides access to logs of the container.
///
/// Most implementations will very likely want to make use of this to wait for a particular
/// message to be emitted.
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>);
/// The conditions returned from this method are evaluated **in the order** they are returned. Therefore
/// you most likely want to start with a [`WaitFor::StdOutMessage`] or [`WaitFor::StdErrMessage`] and
/// potentially follow up with a [`WaitFor::Duration`] in case the container usually needs a little
/// more time before it is ready.
fn ready_conditions(&self) -> Vec<WaitFor>;

/// Returns the arguments this instance was created with.
fn args(&self) -> Self::Args;
Expand Down Expand Up @@ -106,6 +105,58 @@ pub struct Port {
pub internal: u16,
}

/// Represents a condition that needs to be met before a container is considered ready.
#[derive(Debug, PartialEq, Clone)]
pub enum WaitFor {
/// An empty condition. Useful for default cases or fallbacks.
Nothing,
/// Wait for a message on the stdout stream of the container's logs.
StdOutMessage { message: String },
/// Wait for a message on the stderr stream of the container's logs.
StdErrMessage { message: String },
/// Wait for a certain amount of time.
Duration { length: Duration },
}

impl WaitFor {
pub fn message_on_stdout<S: Into<String>>(message: S) -> WaitFor {
WaitFor::StdOutMessage {
message: message.into(),
}
}

pub fn message_on_stderr<S: Into<String>>(message: S) -> WaitFor {
WaitFor::StdErrMessage {
message: message.into(),
}
}

pub fn seconds(length: u64) -> WaitFor {
WaitFor::Duration {
length: Duration::from_secs(length),
}
}

pub fn millis(length: u64) -> WaitFor {
WaitFor::Duration {
length: Duration::from_millis(length),
}
}

pub fn millis_in_env_var(name: &'static str) -> WaitFor {
let additional_sleep_period = var(name).map(|value| value.parse());

(|| {
let length = additional_sleep_period.ok()?.ok()?;

Some(WaitFor::Duration {
length: Duration::from_millis(length),
})
})()
.unwrap_or(WaitFor::Nothing)
}
}

impl Into<Port> for (u16, u16) {
fn into(self) -> Port {
Port {
Expand Down
30 changes: 7 additions & 23 deletions src/images/coblox_bitcoincore.rs
@@ -1,9 +1,9 @@
use crate::core::{Container, Docker, Image, WaitForMessage};
use crate::core::{Image, WaitFor};
use hex::encode;
use hmac::{Hmac, Mac, NewMac};
use rand::{thread_rng, Rng};
use sha2::Sha256;
use std::{collections::HashMap, env::var, fmt, thread::sleep, time::Duration};
use std::{collections::HashMap, fmt};

#[derive(Debug)]
pub struct BitcoinCore {
Expand Down Expand Up @@ -197,27 +197,11 @@ impl Image for BitcoinCore {
format!("coblox/bitcoin-core:{}", self.tag)
}

fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
container
.logs()
.stdout
.wait_for_message("Flushed wallet.dat")
.unwrap();

let additional_sleep_period =
var("BITCOIND_ADDITIONAL_SLEEP_PERIOD").map(|value| value.parse());

if let Ok(Ok(sleep_period)) = additional_sleep_period {
let sleep_period = Duration::from_millis(sleep_period);

log::trace!(
"Waiting for an additional {:?} for container {}.",
sleep_period,
container.id()
);

sleep(sleep_period)
}
fn ready_conditions(&self) -> Vec<WaitFor> {
vec![
WaitFor::message_on_stdout("Flushed wallet.dat"),
WaitFor::millis_in_env_var("BITCOIND_ADDITIONAL_SLEEP_PERIOD"),
]
}

fn args(&self) -> <Self as Image>::Args {
Expand Down
32 changes: 9 additions & 23 deletions src/images/dynamodb_local.rs
@@ -1,7 +1,6 @@
use crate::{Container, Docker, Image, WaitForMessage};
use std::{collections::HashMap, env::var, thread::sleep, time::Duration};
use crate::{core::WaitFor, Image};
use std::collections::HashMap;

const ADDITIONAL_SLEEP_PERIOD: &str = "DYNAMODB_ADDITIONAL_SLEEP_PERIOD";
const CONTAINER_IDENTIFIER: &str = "amazon/dynamodb-local";
const DEFAULT_WAIT: u64 = 2000;
const DEFAULT_TAG: &str = "latest";
Expand Down Expand Up @@ -43,26 +42,13 @@ impl Image for DynamoDb {
format!("{}:{}", CONTAINER_IDENTIFIER, &self.tag)
}

fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
container
.logs()
.stdout
.wait_for_message("Initializing DynamoDB Local with the following configuration")
.unwrap();

let additional_sleep_period = var(ADDITIONAL_SLEEP_PERIOD)
.map(|value| value.parse().unwrap_or(DEFAULT_WAIT))
.unwrap_or(DEFAULT_WAIT);

let sleep_period = Duration::from_millis(additional_sleep_period);

log::trace!(
"Waiting for an additional {:?} for container {}.",
sleep_period,
container.id()
);

sleep(sleep_period)
fn ready_conditions(&self) -> Vec<WaitFor> {
vec![
WaitFor::message_on_stdout(
"Initializing DynamoDB Local with the following configuration",
),
WaitFor::millis(DEFAULT_WAIT),
]
}

fn args(&self) -> <Self as Image>::Args {
Expand Down
10 changes: 3 additions & 7 deletions src/images/elasticmq.rs
@@ -1,4 +1,4 @@
use crate::{Container, Docker, Image, WaitForMessage};
use crate::{core::WaitFor, Image};
use std::collections::HashMap;

const CONTAINER_IDENTIFIER: &str = "softwaremill/elasticmq";
Expand Down Expand Up @@ -41,12 +41,8 @@ impl Image for ElasticMQ {
format!("{}:{}", CONTAINER_IDENTIFIER, &self.tag)
}

fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
container
.logs()
.stdout
.wait_for_message("Started SQS rest server")
.unwrap();
fn ready_conditions(&self) -> Vec<WaitFor> {
vec![WaitFor::message_on_stdout("Started SQS rest server")]
}

fn args(&self) -> <Self as Image>::Args {
Expand Down
50 changes: 6 additions & 44 deletions src/images/generic.rs
@@ -1,51 +1,13 @@
use crate::{Container, Docker, Image, WaitError, WaitForMessage};
use crate::{core::WaitFor, Image};
use std::collections::HashMap;

#[derive(Debug, PartialEq, Clone)]
pub enum WaitFor {
Nothing,
LogMessage { message: String, stream: Stream },
}

#[derive(Debug, PartialEq, Clone)]
pub enum Stream {
StdOut,
StdErr,
}

impl WaitFor {
pub fn message_on_stdout<S: Into<String>>(message: S) -> WaitFor {
WaitFor::LogMessage {
message: message.into(),
stream: Stream::StdOut,
}
}

pub fn message_on_stderr<S: Into<String>>(message: S) -> WaitFor {
WaitFor::LogMessage {
message: message.into(),
stream: Stream::StdErr,
}
}

fn wait<D: Docker, I: Image>(&self, container: &Container<'_, D, I>) -> Result<(), WaitError> {
match self {
WaitFor::Nothing => Ok(()),
WaitFor::LogMessage { message, stream } => match stream {
Stream::StdOut => container.logs().stdout.wait_for_message(message),
Stream::StdErr => container.logs().stderr.wait_for_message(message),
},
}
}
}

#[derive(Debug)]
pub struct GenericImage {
descriptor: String,
arguments: Vec<String>,
volumes: HashMap<String, String>,
env_vars: HashMap<String, String>,
wait_for: WaitFor,
wait_for: Vec<WaitFor>,
entrypoint: Option<String>,
}

Expand All @@ -56,7 +18,7 @@ impl Default for GenericImage {
arguments: vec![],
volumes: HashMap::new(),
env_vars: HashMap::new(),
wait_for: WaitFor::Nothing,
wait_for: Vec::new(),
entrypoint: None,
}
}
Expand All @@ -81,7 +43,7 @@ impl GenericImage {
}

pub fn with_wait_for(mut self, wait_for: WaitFor) -> Self {
self.wait_for = wait_for;
self.wait_for.push(wait_for);
self
}
}
Expand All @@ -96,8 +58,8 @@ impl Image for GenericImage {
self.descriptor.to_owned()
}

fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
self.wait_for.wait(container).unwrap();
fn ready_conditions(&self) -> Vec<WaitFor> {
self.wait_for.clone()
}

fn args(&self) -> Self::Args {
Expand Down
13 changes: 5 additions & 8 deletions src/images/mongo.rs
@@ -1,7 +1,6 @@
use crate::{core::WaitFor, Image};
use std::collections::HashMap;

use crate::{Container, Docker, Image, WaitForMessage};

const CONTAINER_IDENTIFIER: &str = "mongo";
const DEFAULT_TAG: &str = "4.0.17";

Expand Down Expand Up @@ -42,12 +41,10 @@ impl Image for Mongo {
format!("{}:{}", CONTAINER_IDENTIFIER, &self.tag)
}

fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
container
.logs()
.stdout
.wait_for_message("waiting for connections on port")
.unwrap();
fn ready_conditions(&self) -> Vec<WaitFor> {
vec![WaitFor::message_on_stdout(
"waiting for connections on port",
)]
}

fn args(&self) -> <Self as Image>::Args {
Expand Down

0 comments on commit 82ebb99

Please sign in to comment.