Skip to content

Commit

Permalink
Add watchdog feature to allow cleanup on kill
Browse files Browse the repository at this point in the history
The watchdog will spawn a background thread that registers signal
handlers for SIGTERM, SIGINT, and SIGQUIT, if any such signal is
observed it will clean up currently running containers that are not
marked as `Keep`.

Signed-off-by: Heinz N. Gies <heinz@licenser.net>
  • Loading branch information
Licenser committed May 5, 2022
1 parent 2afa2cc commit 2eccc36
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added `watchdog` feature that spawns a background thread keeping track of docker containers that are started by the test suite and removes them in the case of a `CTRL+C` or `kill` of the test process.

## [0.13.0] - 2022-04-04

### Added
Expand Down
4 changes: 4 additions & 0 deletions testcontainers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ description = "A library for integration-testing against docker containers from
async-trait = { version = "0.1", optional = true }
bollard = { version = "0.11", optional = true }
bollard-stubs = "1.41"
conquer-once = { version = "0.3", optional = true }
futures = "0.3"
hex = "0.4"
hmac = "0.12"
Expand All @@ -22,9 +23,12 @@ rand = "0.8"
serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
sha2 = "0.10"
signal-hook = { version = "0.3", optional = true }
tokio = { version = "1", features = [ "macros" ], optional = true }

[features]
default = [ ]
watchdog = [ "signal-hook", "conquer-once" ]
experimental = [ "async-trait", "bollard", "tokio" ]

[dev-dependencies]
Expand Down
6 changes: 6 additions & 0 deletions testcontainers/src/clients/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ impl Cli {
.expect("output is not valid utf8")
.trim()
.to_string();

#[cfg(feature = "watchdog")]
if self.inner.command == env::Command::Remove {
crate::watchdog::register(container_id.clone());
}

self.inner.register_container_started(container_id.clone());

self.block_until_ready(&container_id, image.ready_conditions());
Expand Down
5 changes: 5 additions & 0 deletions testcontainers/src/clients/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ impl Http {
}
};

#[cfg(feature = "watchdog")]
if self.inner.command == env::Command::Remove {
crate::watchdog::Watchdog::register(container_id.clone());
}

self.inner
.bollard
.start_container::<String>(&id, None)
Expand Down
2 changes: 2 additions & 0 deletions testcontainers/src/core/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ where
Command::Keep => {}
Command::Remove => self.rm(),
}
#[cfg(feature = "watchdog")]
crate::watchdog::unregister(self.id());
}
}

Expand Down
2 changes: 2 additions & 0 deletions testcontainers/src/core/container_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ where
env::Command::Remove => self.docker_client.rm(&self.id).await,
env::Command::Keep => {}
}
#[cfg(feature = "watchdog")]
crate::watchdog::unregister(self.id());
}
}

Expand Down
3 changes: 3 additions & 0 deletions testcontainers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ pub use crate::core::{Container, Image, ImageArgs, RunnableImage};
#[cfg(feature = "experimental")]
pub use crate::core::ContainerAsync;

#[cfg(feature = "watchdog")]
pub(crate) mod watchdog;

/// All available Docker clients.
pub mod clients;
pub mod core;
Expand Down
52 changes: 52 additions & 0 deletions testcontainers/src/watchdog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::{clients::Cli, core::Docker};
use conquer_once::Lazy;
use signal_hook::{
consts::{SIGINT, SIGQUIT, SIGTERM},
iterator::Signals,
};
use std::{collections::BTreeSet, sync::Mutex, thread};

static WATCHDOG: Lazy<Mutex<Watchdog>> = Lazy::new(|| {
thread::spawn(move || {
let signal_docker = Cli::default();
let mut signals =
Signals::new(&[SIGTERM, SIGINT, SIGQUIT]).expect("failed to register signal handler");

for signal in &mut signals {
for container_id in WATCHDOG
.lock()
.map(|s| s.containers.clone())
.unwrap_or_default()
{
signal_docker.stop(&container_id);
signal_docker.rm(&container_id);
}

let _ = signal_hook::low_level::emulate_default_handler(signal);
}
});

Mutex::new(Watchdog::default())
});

#[derive(Default)]
pub(crate) struct Watchdog {
containers: BTreeSet<String>,
}

/// Register a container for observation
pub(crate) fn register(container_id: String) {
WATCHDOG
.lock()
.expect("failed to access watchdog")
.containers
.insert(container_id);
}
/// Unregisters a container for observation
pub(crate) fn unregister(container_id: &str) {
WATCHDOG
.lock()
.expect("failed to access watchdog")
.containers
.remove(container_id);
}

0 comments on commit 2eccc36

Please sign in to comment.