Skip to content

Commit

Permalink
feat(jstzd): implement async-drop for container
Browse files Browse the repository at this point in the history
  • Loading branch information
ryutamago committed Sep 19, 2024
1 parent c06bb9a commit 25d0429
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 54 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ actix-web = "4.5.1"
actix-web-lab = "0.20.0"
ansi_term = "0.12.1"
anyhow = "1.0.82"
async-scoped = "0.9.0"
base64 = "0.21.7"
bincode = "1.3.3"
boa_engine = { version = "0.17.0", features = ["fuzz"] }
Expand Down
1 change: 1 addition & 0 deletions crates/jstzd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repository.workspace = true
anyhow.workspace = true
async-dropper.workspace = true
async-trait.workspace = true
async-scoped.workspace = true
bollard.workspace = true
bytes.workspace = true
futures-util.workspace = true
Expand Down
50 changes: 44 additions & 6 deletions crates/jstzd/src/docker/container.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
use anyhow::Result;
use async_dropper::AsyncDrop;
use async_scoped::TokioScope;
use async_trait::async_trait;
use bollard::Docker;
use log::error;
use std::sync::Arc;

pub struct Container {
pub id: String,
client: Option<Arc<Docker>>,
dropped: bool,
_private: (),
}

impl Default for Container {
fn default() -> Self {
Self {
id: String::new(),
client: None,
dropped: false,
_private: (),
}
}
}

impl Container {
/// Creates a new container with running `id`
pub(super) fn new(client: Arc<Docker>, id: String) -> Self {
Self {
id,
client: Some(client),
dropped: false,
_private: (),
}
}
Expand Down Expand Up @@ -48,15 +65,36 @@ impl Container {
}
}

// Stop and remove the container, should be called when dropping the container
pub async fn cleanup(&self) -> Result<()> {
self.stop().await?;
self.remove().await
}

fn client(&self) -> Result<&Arc<Docker>> {
self.client
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Client does not exist"))
}
}

#[async_trait]
impl AsyncDrop for Container {
async fn async_drop(&mut self) {
self.stop().await.unwrap_or_else(|e| error!("{}", e));
self.remove().await.unwrap_or_else(|e| error!("{}", e));
}
}

impl Drop for Container {
// same drop implementation as in async-dropper-simple crate
// https://github.com/t3hmrman/async-dropper/blob/ec6e5bbd6c894b23538cfec80375bcaefb8e5710/crates/async-dropper-simple/src/no_default_bound.rs#L111
fn drop(&mut self) {
if !self.dropped {
// Prevent the copy `this` to drop again
self.dropped = true;
let mut this = std::mem::take(self);
// Prevent the original `self` to drop again
self.dropped = true;
TokioScope::scope_and_block(|s| {
s.spawn(async move {
this.async_drop().await;
})
});
}
}
}
94 changes: 46 additions & 48 deletions crates/jstzd/tests/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,69 +5,34 @@ use bollard::{container::ListContainersOptions, Docker};
use jstzd::docker::{GenericImage, Image, RunnableImage};
use serial_test::serial;

#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn creates_container() -> Result<()> {
let docker = docker();
let image = image(docker.clone()).await?;
let runnable_image = RunnableImage::new(image.clone(), "test_container1");
let container = runnable_image.create_container(docker.clone()).await?;
let containers = docker
.list_containers(Some(ListContainersOptions::<String> {
all: true,
..Default::default()
}))
.await?;
assert!(containers
.iter()
.any(|c| c.id.as_deref() == Some(&container.id)));
container.cleanup().await?;
let option = Some(ListContainersOptions::<String> {
all: true,
..Default::default()
});
assert!(container_exists(docker, &container.id, option).await);
Ok(())
}

#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn runs_container() -> Result<()> {
let docker = docker();
let image = image(docker.clone()).await?;
let cmd = vec!["sh".to_string(), "-c".to_string(), "sleep 1".to_string()];
let runnable_image =
RunnableImage::new(image.clone(), "test_container2").with_overridden_cmd(cmd);
let runnable_image = RunnableImage::new(image.clone(), "test_container2");
let container = runnable_image.create_container(docker.clone()).await?;
container.start().await?;
let containers = docker
.list_containers(Some(ListContainersOptions::<String> {
..Default::default()
}))
.await?;
assert!(containers
.iter()
.any(|c| c.id.as_deref() == Some(&container.id)));
container.cleanup().await?;
assert!(container_exists(docker, &container.id, None).await);
Ok(())
}

#[tokio::test]
#[serial]
async fn cleans_up_container() -> Result<()> {
let docker = docker();
let image = image(docker.clone()).await?;
let runnable_image = RunnableImage::new(image.clone(), "test_container3");
let container = runnable_image.create_container(docker.clone()).await?;
container.cleanup().await?;
let containers = docker
.list_containers(Some(ListContainersOptions::<String> {
all: true,
..Default::default()
}))
.await?;
assert!(containers
.iter()
.any(|c| c.id.as_deref() != Some(&container.id)));
Ok(())
}

#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn removing_container_twice_fails() -> Result<()> {
let docker = docker();
Expand All @@ -86,7 +51,7 @@ async fn removing_container_twice_fails() -> Result<()> {
Ok(())
}

#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn removing_running_container_fails() -> Result<()> {
let docker = docker();
Expand All @@ -103,11 +68,28 @@ async fn removing_running_container_fails() -> Result<()> {
.unwrap()
.to_string()
.contains("stop the container before removing"));
container.cleanup().await?;
Ok(())
}

#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn stops_and_removes_container_on_drop() -> Result<()> {
let docker = docker();
let image = image(docker.clone()).await?;
let runnable_image = RunnableImage::new(image.clone(), "test_container3");
let container = runnable_image.create_container(docker.clone()).await?;
let container_id = container.id.clone();
container.start().await?;
std::mem::drop(container);
let options = Some(ListContainersOptions::<String> {
all: true,
..Default::default()
});
assert!(!container_exists(docker, &container_id, options).await);
Ok(())
}

#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn running_container_with_no_image_fails() -> Result<()> {
let docker = docker();
Expand Down Expand Up @@ -135,3 +117,19 @@ async fn image(client: Arc<Docker>) -> Result<GenericImage> {
image.pull_image(client).await?;
Ok(image)
}

async fn container_exists(
client: Arc<Docker>,
container_id: &str,
options: Option<ListContainersOptions<String>>,
) -> bool {
let containers = client
.list_containers(options.or(Some(ListContainersOptions::<String> {
..Default::default()
})))
.await
.unwrap();
containers
.iter()
.any(|c| c.id.as_deref() == Some(container_id))
}

0 comments on commit 25d0429

Please sign in to comment.