Repository: DataDog/lading
Pull request #1325: [CONTINT-4562] Add a parameter to continuously stop and recreate containers
Author: login: L3n41c
Labels: name: mergequeue-status: done
Created: 2025-04-22T09:51:18Z | Merged: 2025-04-29T11:21:27Z

### What does this PR do?

Add a new `max_lifetime` parameter to the `container` generator.
This will make `lading` endlessly delete the oldest container and replace it with a newer one.

### Motivation

Some memory leaks inside the agent might happen each time a new container is detected or each time an old container is deleted.
In order to catch them, having a a static set of running containers during the duration of an experiment might not be enough.
We would like to generate a continuous stream of container deletion and container re-creation events.

### Related issues

### Additional Notes


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49117c5aa..141eb26f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,13 +5,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+## Added
+- Compute the Working Set Size of the target thanks to the Linux Idle Page Tracking API.
+  And expose it in the new `total_wss_bytes` metric.
+- Add a `max_lifetime` parameter to the `container` generator to make it generate
+  a continuous stream of docker containers deletion and re-creation.
 
 ## [0.25.9]
 ## Added
 - Introduce a `container` generator able to generate an arbitrary number
-  of docker containers
-- Compute the Working Set Size of the target thanks to the Linux Idle Page Tracking API.
-  And expose it in the new `total_wss_bytes` metric.
+  of docker containers.
 ## Changed
 - Lading's byte-unit crate is now updated to 5.x. This version of the crate is
   very strict about the difference between MiB, Mb etc.
diff --git a/examples/lading-container.yaml b/examples/lading-container.yaml
index c86c437d8..d0883c073 100644
--- a/examples/lading-container.yaml
+++ b/examples/lading-container.yaml
@@ -19,4 +19,5 @@ generator:
       network_disabled: false
       exposed_ports:
         - 5000/tcp
+      max_lifetime_seconds: 20
       number_of_containers: 10
diff --git a/lading/src/generator/container.rs b/lading/src/generator/container.rs
index 37f6790f0..5b5521d85 100644
--- a/lading/src/generator/container.rs
+++ b/lading/src/generator/container.rs
@@ -12,14 +12,16 @@ use bollard::container::{
 use bollard::image::CreateImageOptions;
 use bollard::secret::ContainerCreateResponse;
 use serde::{Deserialize, Serialize};
-use std::collections::HashMap;
+use std::collections::{HashMap, VecDeque};
+use tokio::sync::mpsc;
+use tokio::task::JoinSet;
 use tokio_stream::StreamExt;
 use tracing::{debug, info, warn};
 use uuid::Uuid;
 
 use super::General;
 
-#[derive(Debug, Deserialize, Serialize, PartialEq)]
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
 #[serde(deny_unknown_fields)]
 /// Configuration of the container generator.
 pub struct Config {
@@ -37,8 +39,10 @@ pub struct Config {
     pub network_disabled: Option<bool>,
     /// Ports to expose from the container
     pub exposed_ports: Option<Vec<String>>,
+    /// Maximum lifetime of containers before being replaced
+    pub max_lifetime_seconds: Option<std::num::NonZeroU64>,
     /// Number of containers to spin up (defaults to 1)
-    pub number_of_containers: Option<u32>,
+    pub number_of_containers: Option<std::num::NonZeroU32>,
 }
 
 /// Errors produced by the `Container` generator.
@@ -50,19 +54,15 @@ pub enum Error {
     /// Error produced by the Bollard Docker client.
     #[error("Bollard/Docker error: {0}")]
     Bollard(#[from] bollard::errors::Error),
+    /// Error produced by tokio
+    #[error("Tokio error: {0}")]
+    Tokio(#[from] mpsc::error::SendError<ContainerCreateResponse>),
 }
 
 #[derive(Debug)]
 /// Represents a container that can be spun up from a configured image.
 pub struct Container {
-    image: String,
-    tag: String,
-    args: Option<Vec<String>>,
-    env: Option<Vec<String>>,
-    labels: Option<HashMap<String, String>>,
-    network_disabled: Option<bool>,
-    exposed_ports: Option<Vec<String>>,
-    number_of_containers: usize,
+    config: Config,
     shutdown: lading_signal::Watcher,
 }
 
@@ -80,50 +80,11 @@ impl Container {
         shutdown: lading_signal::Watcher,
     ) -> Result<Self, Error> {
         Ok(Self {
-            image: config.repository.clone(),
-            tag: config.tag.clone(),
-            args: config.args.clone(),
-            env: config.env.clone(),
-            labels: config.labels.clone(),
-            network_disabled: config.network_disabled,
-            exposed_ports: config.exposed_ports.clone(),
-            number_of_containers: config.number_of_containers.unwrap_or(1) as usize,
+            config: config.clone(),
             shutdown,
         })
     }
 
-    /// Convert the `Container` instance to a `ContainerConfig` for the Docker API.
-    #[must_use]
-    fn to_container_config<'a>(&'a self, full_image: &'a str) -> ContainerConfig<&'a str> {
-        ContainerConfig {
-            image: Some(full_image),
-            tty: Some(true),
-            cmd: self
-                .args
-                .as_ref()
-                .map(|args| args.iter().map(String::as_str).collect()),
-            env: self
-                .env
-                .as_ref()
-                .map(|env| env.iter().map(String::as_str).collect()),
-            labels: self.labels.as_ref().map(|labels| {
-                labels
-                    .iter()
-                    .map(|(key, value)| (key.as_str(), value.as_str()))
-                    .collect()
-            }),
-            network_disabled: self.network_disabled,
-            #[allow(clippy::zero_sized_map_values)]
-            exposed_ports: self.exposed_ports.as_ref().map(|ports| {
-                ports
-                    .iter()
-                    .map(|port| (port.as_str(), HashMap::new()))
-                    .collect()
-            }),
-            ..Default::default()
-        }
-    }
-
     /// Run the `Container` generator.
     ///
     /// # Errors
@@ -131,6 +92,8 @@ impl Container {
     /// Will return an error if Docker connection fails, image pulling fails,
     /// container creation fails, container start fails, or container removal fails.
     ///
+    /// # Panics
+    ///
     /// Steps:
     /// 1. Connect to Docker.
     /// 2. Pull the specified image (if not available).
@@ -138,77 +101,82 @@ impl Container {
     /// 4. Wait for shutdown signal.
     /// 5. On shutdown, stop and remove the container.
     pub async fn spin(self) -> Result<(), Error> {
-        info!("Container generator running: {}:{}", self.image, self.tag);
+        info!(
+            "Container generator running: {}:{}",
+            self.config.repository, self.config.tag
+        );
 
         let docker = Docker::connect_with_local_defaults()?;
 
-        let full_image = format!("{}:{}", self.image, self.tag);
+        let full_image = format!("{}:{}", self.config.repository, self.config.tag);
         debug!("Ensuring image is available: {full_image}");
 
-        // Pull the image
-        let mut pull_stream = docker.create_image(
-            Some(CreateImageOptions::<String> {
-                from_image: full_image.clone(),
-                ..Default::default()
-            }),
-            None,
-            None,
-        );
-
-        while let Some(item) = pull_stream.next().await {
-            match item {
-                Ok(status) => {
-                    if let Some(progress) = status.progress {
-                        info!("Pull progress: {progress}");
-                    }
-                }
-                Err(e) => {
-                    warn!("Pull error: {e}");
-                    return Err(e.into());
-                }
-            }
-        }
-
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
+        pull_image(&docker, &full_image).await?;
 
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
+        #[allow(clippy::unwrap_used)]
+        let number_of_containers = self
+            .config
+            .number_of_containers
+            .unwrap_or(std::num::NonZero::new(1).unwrap());
 
-            containers.push(container);
+        let mut set = JoinSet::new();
+        for _ in 0..number_of_containers.into() {
+            let docker = docker.clone();
+            let config = self.config.clone();
+            let full_image = full_image.clone();
+            set.spawn(async move {
+                config
+                    .create_and_start_container(&docker, &full_image)
+                    .await
+            });
         }
 
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let mut containers = set
+            .join_all()
+            .await
+            .into_iter()
+            .collect::<Result<VecDeque<ContainerCreateResponse>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self
+            .config
+            .max_lifetime_seconds
+            .map(|max_lifetime_seconds| {
+                tokio::time::interval(
+                    tokio::time::Duration::from_secs(max_lifetime_seconds.into())
+                        / <std::num::NonZero<u32> as Into<u32>>::into(number_of_containers),
+                )
+            });
+        let mut liveness_interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
         loop {
             tokio::select! {
+                // Destroy and replace containers
+                _ = if let Some(ref mut interval) = recreate_interval { interval.tick() } else { std::future::pending().await } => {
+                    if let Some(container) = containers.pop_front() {
+                        let docker = docker.clone();
+                        let config = self.config.clone();
+                        let full_image = full_image.clone();
+                        let new_container_tx = new_container_tx.clone();
+                        tokio::spawn(async move {
+                            stop_and_remove_container(&docker, &container).await?;
+                            new_container_tx.send(config.create_and_start_container(&docker, &full_image).await?).await?;
+                            Ok::<(), Error>(())
+                        });
+                    }
+                }
+
+                // Handle new containers from recycling tasks
+                new_container = new_container_rx.recv() => {
+                    if let Some(container) = new_container {
+                        containers.push_back(container);
+                    }
+                }
+
                 // Check that containers are still running every 10 seconds
-                _ = interval.tick() => {
+                _ = liveness_interval.tick() => {
                     for container in &containers {
                         if let Some(state) = docker.inspect_container(&container.id, None).await?.state {
                             if !state.running.unwrap_or(false) {
@@ -220,42 +188,146 @@ impl Container {
                         }
                     }
                 }
+
+                // Shutdown
                 () = &mut shutdown_wait => {
                     debug!("shutdown signal received");
-                    for container in &containers {
-                        Self::stop_and_remove_container(&docker, container).await?;
+                    drop(new_container_tx);
+
+                    let mut set = JoinSet::new();
+                    for container in containers {
+                        let docker = docker.clone();
+                        set.spawn(async move {
+                            stop_and_remove_container(&docker, &container).await
+                        });
                     }
 
+                    set.join_all().await;
+
                     return Ok(())
                 }
             }
         }
     }
+}
 
-    async fn stop_and_remove_container(
-        docker: &Docker,
-        container: &ContainerCreateResponse,
-    ) -> Result<(), Error> {
-        info!("Stopping container: {id}", id = container.id);
-        if let Err(e) = docker
-            .stop_container(&container.id, Some(StopContainerOptions { t: 5 }))
-            .await
-        {
-            warn!("Error stopping container {id}: {e}", id = container.id);
+async fn pull_image(docker: &Docker, full_image: &str) -> Result<(), Error> {
+    let mut pull_stream = docker.create_image(
+        Some(CreateImageOptions::<&str> {
+            from_image: full_image,
+            ..Default::default()
+        }),
+        None,
+        None,
+    );
+
+    while let Some(item) = pull_stream.next().await {
+        match item {
+            Ok(status) => {
+                if let Some(progress) = status.progress {
+                    info!("Pull progress: {progress}");
+                }
+            }
+            Err(e) => {
+                warn!("Pull error: {e}");
+                return Err(e.into());
+            }
         }
+    }
 
-        debug!("Removing container: {id}", id = container.id);
-        docker
-            .remove_container(
-                &container.id,
-                Some(RemoveContainerOptions {
-                    force: true,
-                    ..Default::default()
+    Ok(())
+}
+
+impl Config {
+    /// Convert the `Container` instance to a `ContainerConfig` for the Docker API.
+    #[must_use]
+    fn to_container_config<'a>(&'a self, full_image: &'a str) -> ContainerConfig<&'a str> {
+        ContainerConfig {
+            image: Some(full_image),
+            tty: Some(true),
+            cmd: self
+                .args
+                .as_ref()
+                .map(|args| args.iter().map(String::as_str).collect()),
+            env: self
+                .env
+                .as_ref()
+                .map(|env| env.iter().map(String::as_str).collect()),
+            labels: self.labels.as_ref().map(|labels| {
+                labels
+                    .iter()
+                    .map(|(key, value)| (key.as_str(), value.as_str()))
+                    .collect()
+            }),
+            network_disabled: self.network_disabled,
+            #[allow(clippy::zero_sized_map_values)]
+            exposed_ports: self.exposed_ports.as_ref().map(|ports| {
+                ports
+                    .iter()
+                    .map(|port| (port.as_str(), HashMap::new()))
+                    .collect()
+            }),
+            ..Default::default()
+        }
+    }
+
+    async fn create_and_start_container(
+        &self,
+        docker: &Docker,
+        full_image: &str,
+    ) -> Result<ContainerCreateResponse, Error> {
+        let container_name = format!("lading_container_{}", Uuid::new_v4());
+        debug!("Creating container: {container_name}");
+
+        let container = docker
+            .create_container(
+                Some(CreateContainerOptions {
+                    name: &container_name,
+                    platform: None,
                 }),
+                self.to_container_config(full_image),
             )
             .await?;
 
-        debug!("Removed container: {id}", id = container.id);
-        Ok(())
+        debug!("Created container with id: {id}", id = container.id);
+        for warning in &container.warnings {
+            warn!("Container warning: {warning}");
+        }
+
+        docker
+            .start_container(&container.id, None::<StartContainerOptions<String>>)
+            .await?;
+
+        debug!("Started container: {id}", id = container.id);
+
+        Ok(container)
     }
 }
+
+async fn stop_and_remove_container(
+    docker: &Docker,
+    container: &ContainerCreateResponse,
+) -> Result<(), Error> {
+    info!("Stopping container: {id}", id = container.id);
+    if let Err(e) = docker
+        .stop_container(&container.id, Some(StopContainerOptions { t: 5 }))
+        .await
+    {
+        warn!("Error stopping container {id}: {e}", id = container.id);
+    }
+
+    debug!("Removing container: {id}", id = container.id);
+    docker
+        .remove_container(
+            &container.id,
+            Some(RemoveContainerOptions {
+                force: true,
+                ..Default::default()
+            }),
+        )
+        .await?;
+
+    debug!("Removed container: {id}", id = container.id);
+
+    Ok(())
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a new max_lifetime parameter to the container generator to enable continuous container recycling for better detection of memory leaks. Key changes include refactoring the Container struct to rely on a Config object, updating container creation with asynchronous tasks for recreation based on the max_lifetime, and updating examples and the changelog to document the new parameter.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

| File                                | Description                                                                      |
| ----------------------------------- | -------------------------------------------------------------------------------- |
| lading/src/generator/container.rs   | Added a max_lifetime parameter, refactored the container struct and container lifecycle logic to support container recycling. |
| examples/lading-container.yaml      | Updated the YAML configuration example to include the max_lifetime parameter.      |
| CHANGELOG.md                       | Updated changelog to document the addition of the max_lifetime feature.           |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**lading/src/generator/container.rs:150**
* The computed recreation interval may result in a zero duration if max_lifetime is less than number_of_containers. Consider enforcing a minimum interval to prevent unintended rapid container recycling.
```
tokio::time::Duration::from_millis(1_000 * max_lifetime / number_of_containers as u64)
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-22T14:53:33Z
user: login: scottopell
body: Looks good overall, only had time for a brief pass
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:07:52Z
user: login: blt
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T15:30:57Z
user: login: blt
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T15:45:52Z
user: login: goxberry
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:21:51Z
user: login: goxberry
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:36:19Z
user: login: goxberry
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:44:50Z
user: login: goxberry
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:54:15Z
user: login: L3n41c
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T15:21:39Z
user: login: L3n41c
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T19:00:23Z
user: login: L3n41c
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T19:01:23Z
user: login: L3n41c
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T19:02:08Z
user: login: L3n41c
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T19:02:38Z
user: login: scottopell
body: looks good to me pending @blt 's approval on the select interval bit.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T19:40:07Z
user: login: L3n41c
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T20:31:07Z
user: login: blt
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-28T16:09:40Z
user: login: L3n41c
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T10:38:52Z

Review comments:
diff_hunk: @@ -169,46 +132,51 @@ impl Container {
             }
         }
 
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
-
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
-
-            containers.push(container);
-        }
-
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let number_of_containers = self.config.number_of_containers.unwrap_or(1);
+        let mut containers = futures::future::join_all(
+            (0..number_of_containers)
+                .map(|_| self.config.create_and_start_container(&docker, &full_image)),
+        )
+        .await
+        .into_iter()
+        .collect::<Result<VecDeque<_>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self.config.max_lifetime.map(|max_lifetime| {
+            tokio::time::interval(tokio::time::Duration::from_millis(
+                1_000 * max_lifetime / number_of_containers as u64,
+            ))
+        });
+        let mut liveness_interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
         loop {
             tokio::select! {
+                // Destroy and replace containers
+                _ = if let Some(ref mut interval) = recreate_interval { interval.tick() } else { std::future::pending().await } => {
+                    if let Some(container) = containers.pop_front() {
+                        let docker = docker.clone();
+                        let config = self.config.clone();
+                        let full_image = full_image.clone();
+                        let new_container_tx = new_container_tx.clone();
+                        tokio::spawn(async move {
+                            stop_and_remove_container(&docker, &container).await?;
+                            new_container_tx.send(config.create_and_start_container(&docker, &full_image).await?).await?;
+                            Ok::<(), Error>(())
+                        });
+                    }
path: lading/src/generator/container.rs
user: login: Copilot
body: Errors in the spawned task for container recreation are not awaited or logged, which could make debugging difficult. Consider capturing the JoinHandle and handling any errors from the asynchronous task.
```suggestion
                        let handle = tokio::spawn(async move {
                            stop_and_remove_container(&docker, &container).await?;
                            new_container_tx.send(config.create_and_start_container(&docker, &full_image).await?).await?;
                            Ok::<(), Error>(())
                        });
                        task_handles.push(handle);
```
created_at: 2025-04-22T14:53:33Z
author_association: NONE
diff_hunk: @@ -19,4 +19,5 @@ generator:
       network_disabled: false
       exposed_ports:
         - 5000/tcp
+      max_lifetime: 20
path: examples/lading-container.yaml
user: login: scottopell
body: ```suggestion
      max_lifetime_seconds: 20
```

would prefer to be explicit so the units are clear
created_at: 2025-04-22T15:02:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,8 +38,10 @@ pub struct Config {
     pub network_disabled: Option<bool>,
     /// Ports to expose from the container
     pub exposed_ports: Option<Vec<String>>,
+    /// Maximum lifetime of containers before being replaced
+    pub max_lifetime: Option<u64>,
     /// Number of containers to spin up (defaults to 1)
-    pub number_of_containers: Option<u32>,
+    pub number_of_containers: Option<usize>,
path: lading/src/generator/container.rs
user: login: scottopell
body: `u32` makes more sense to me here, this isn't something that would vary with CPU architecture.
created_at: 2025-04-22T15:03:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,8 +38,10 @@ pub struct Config {
     pub network_disabled: Option<bool>,
     /// Ports to expose from the container
     pub exposed_ports: Option<Vec<String>>,
+    /// Maximum lifetime of containers before being replaced
+    pub max_lifetime: Option<u64>,
     /// Number of containers to spin up (defaults to 1)
-    pub number_of_containers: Option<u32>,
+    pub number_of_containers: Option<usize>,
path: lading/src/generator/container.rs
user: login: blt
body: +1
created_at: 2025-04-22T15:30:57Z
author_association: COLLABORATOR
diff_hunk: @@ -169,46 +132,51 @@ impl Container {
             }
         }
 
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
-
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
-
-            containers.push(container);
-        }
-
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let number_of_containers = self.config.number_of_containers.unwrap_or(1);
+        let mut containers = futures::future::join_all(
+            (0..number_of_containers)
+                .map(|_| self.config.create_and_start_container(&docker, &full_image)),
+        )
+        .await
+        .into_iter()
+        .collect::<Result<VecDeque<_>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self.config.max_lifetime.map(|max_lifetime| {
+            tokio::time::interval(tokio::time::Duration::from_millis(
path: lading/src/generator/container.rs
user: login: blt
body: If we're needing to multiple here by 1_000 it's not clear to me that `from_millis` is the correct `from_`.
created_at: 2025-04-22T15:37:55Z
author_association: COLLABORATOR
diff_hunk: @@ -169,46 +132,51 @@ impl Container {
             }
         }
 
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
-
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
-
-            containers.push(container);
-        }
-
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let number_of_containers = self.config.number_of_containers.unwrap_or(1);
+        let mut containers = futures::future::join_all(
+            (0..number_of_containers)
+                .map(|_| self.config.create_and_start_container(&docker, &full_image)),
+        )
+        .await
+        .into_iter()
+        .collect::<Result<VecDeque<_>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self.config.max_lifetime.map(|max_lifetime| {
+            tokio::time::interval(tokio::time::Duration::from_millis(
+                1_000 * max_lifetime / number_of_containers as u64,
+            ))
+        });
+        let mut liveness_interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
         loop {
             tokio::select! {
+                // Destroy and replace containers
+                _ = if let Some(ref mut interval) = recreate_interval { interval.tick() } else { std::future::pending().await } => {
path: lading/src/generator/container.rs
user: login: blt
body: `recreate_interval { interval.tick() } else { std::future::pending().await }`

I'm confused by this. What's this construct doing? 
created_at: 2025-04-22T15:39:46Z
author_association: COLLABORATOR
diff_hunk: @@ -220,42 +188,112 @@ impl Container {
                         }
                     }
                 }
+
+                // Shutdown
                 () = &mut shutdown_wait => {
                     debug!("shutdown signal received");
-                    for container in &containers {
-                        Self::stop_and_remove_container(&docker, container).await?;
-                    }
+                    drop(new_container_tx);
+
+                    futures::future::join_all(
path: lading/src/generator/container.rs
user: login: blt
body: Instead of using the `futures` crate directly, please use `tokio`'s machinery. In this case you'd want a [JoinSet](https://docs.rs/tokio/latest/tokio/task/struct.JoinSet.html).
created_at: 2025-04-22T15:45:32Z
author_association: COLLABORATOR
diff_hunk: @@ -37,8 +38,10 @@ pub struct Config {
     pub network_disabled: Option<bool>,
     /// Ports to expose from the container
     pub exposed_ports: Option<Vec<String>>,
+    /// Maximum lifetime of containers before being replaced
+    pub max_lifetime: Option<u64>,
     /// Number of containers to spin up (defaults to 1)
-    pub number_of_containers: Option<u32>,
+    pub number_of_containers: Option<usize>,
path: lading/src/generator/container.rs
user: login: goxberry
body: `std::num::NonzeroU32` makes more sense to me here unless we want to allow users to set the number of containers to zero.
created_at: 2025-04-22T16:21:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -169,46 +132,51 @@ impl Container {
             }
         }
 
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
-
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
-
-            containers.push(container);
-        }
-
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let number_of_containers = self.config.number_of_containers.unwrap_or(1);
+        let mut containers = futures::future::join_all(
+            (0..number_of_containers)
+                .map(|_| self.config.create_and_start_container(&docker, &full_image)),
+        )
+        .await
+        .into_iter()
+        .collect::<Result<VecDeque<_>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self.config.max_lifetime.map(|max_lifetime| {
+            tokio::time::interval(tokio::time::Duration::from_millis(
path: lading/src/generator/container.rs
user: login: goxberry
body: `core::time::Duration` implements `Div<u32>` and does the right thing [internally](https://doc.rust-lang.org/nightly/src/core/time.rs.html#800-817), so `core::time::Duration::from_secs` seems more appropriate here.
created_at: 2025-04-22T16:36:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -169,46 +132,51 @@ impl Container {
             }
         }
 
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
-
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
-
-            containers.push(container);
-        }
-
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let number_of_containers = self.config.number_of_containers.unwrap_or(1);
+        let mut containers = futures::future::join_all(
+            (0..number_of_containers)
+                .map(|_| self.config.create_and_start_container(&docker, &full_image)),
+        )
+        .await
+        .into_iter()
+        .collect::<Result<VecDeque<_>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self.config.max_lifetime.map(|max_lifetime| {
+            tokio::time::interval(tokio::time::Duration::from_millis(
+                1_000 * max_lifetime / number_of_containers as u64,
path: lading/src/generator/container.rs
user: login: goxberry
body: Forbidding `number_of_containers == 0` would avoid a panic here.
created_at: 2025-04-22T16:44:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -169,46 +132,51 @@ impl Container {
             }
         }
 
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
-
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
-
-            containers.push(container);
-        }
-
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let number_of_containers = self.config.number_of_containers.unwrap_or(1);
+        let mut containers = futures::future::join_all(
+            (0..number_of_containers)
+                .map(|_| self.config.create_and_start_container(&docker, &full_image)),
+        )
+        .await
+        .into_iter()
+        .collect::<Result<VecDeque<_>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self.config.max_lifetime.map(|max_lifetime| {
+            tokio::time::interval(tokio::time::Duration::from_millis(
+                1_000 * max_lifetime / number_of_containers as u64,
+            ))
+        });
+        let mut liveness_interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
         loop {
             tokio::select! {
+                // Destroy and replace containers
+                _ = if let Some(ref mut interval) = recreate_interval { interval.tick() } else { std::future::pending().await } => {
path: lading/src/generator/container.rs
user: login: goxberry
body: The first tick of a `tokio::time::Interval` [returns immediately](https://docs.rs/tokio/latest/tokio/time/struct.Interval.html#examples), so the select will choose among the first `recreate_interval` tick and the first `liveness_interval` tick arbitrarily. Is that behavior intended?
created_at: 2025-04-22T16:54:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -169,46 +132,51 @@ impl Container {
             }
         }
 
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
-
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
-
-            containers.push(container);
-        }
-
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let number_of_containers = self.config.number_of_containers.unwrap_or(1);
+        let mut containers = futures::future::join_all(
+            (0..number_of_containers)
+                .map(|_| self.config.create_and_start_container(&docker, &full_image)),
+        )
+        .await
+        .into_iter()
+        .collect::<Result<VecDeque<_>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self.config.max_lifetime.map(|max_lifetime| {
+            tokio::time::interval(tokio::time::Duration::from_millis(
+                1_000 * max_lifetime / number_of_containers as u64,
+            ))
+        });
+        let mut liveness_interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
         loop {
             tokio::select! {
+                // Destroy and replace containers
+                _ = if let Some(ref mut interval) = recreate_interval { interval.tick() } else { std::future::pending().await } => {
path: lading/src/generator/container.rs
user: login: L3n41c
body: @blt The user may or may not want the containers to be continuously re-created.
That’s why the `max_lifetime_seconds` parameter is an `Option<NonZero<u64>>`.
This `Option`al integer is turned into an `recreate_interval: Option<Interval>` few lines above.

So, now, I’m looking for a way to express in the `tokio::select!` block the fact that I want an arm to be triggered by `interval.tick()` if `Some(interval) = recreate_interval`.
But if `None = recreate_interval`, then I want this arm to be disabled.

Basically, I *think* that what I need in the `else` block is something that has the same type as `interval.tick()` but that never fires.

What would be the most idiomatic way to have a `tokio::select!` arm triggered by an `Option<Interval>` ?

@goxberry I think it doesn’t matter which one of the `recreate_interval` tick or the `liveness_interval` one is executed first. Will the other be properly executed at the next `loop` iteration?
created_at: 2025-04-25T15:21:39Z
author_association: MEMBER
diff_hunk: @@ -220,42 +188,112 @@ impl Container {
                         }
                     }
                 }
+
+                // Shutdown
                 () = &mut shutdown_wait => {
                     debug!("shutdown signal received");
-                    for container in &containers {
-                        Self::stop_and_remove_container(&docker, container).await?;
-                    }
+                    drop(new_container_tx);
+
+                    futures::future::join_all(
path: lading/src/generator/container.rs
user: login: L3n41c
body: Thanks for the tip!
I’ve migrated to `JoinSet` in https://github.com/DataDog/lading/pull/1325/commits/fea93299c54706d7b6aac3303ed9c80d793f00b0#diff-884b2b115b4cc2ae969b3beed17e0ea31dea56389e3948697694c795ebc7eb73R144-R160.
created_at: 2025-04-25T19:00:23Z
author_association: MEMBER
diff_hunk: @@ -169,46 +132,51 @@ impl Container {
             }
         }
 
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
-
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
-
-            containers.push(container);
-        }
-
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let number_of_containers = self.config.number_of_containers.unwrap_or(1);
+        let mut containers = futures::future::join_all(
+            (0..number_of_containers)
+                .map(|_| self.config.create_and_start_container(&docker, &full_image)),
+        )
+        .await
+        .into_iter()
+        .collect::<Result<VecDeque<_>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self.config.max_lifetime.map(|max_lifetime| {
+            tokio::time::interval(tokio::time::Duration::from_millis(
+                1_000 * max_lifetime / number_of_containers as u64,
path: lading/src/generator/container.rs
user: login: L3n41c
body: I’ve made `number_of_containers` a `NonZero` in https://github.com/DataDog/lading/pull/1325/commits/fea93299c54706d7b6aac3303ed9c80d793f00b0#diff-884b2b115b4cc2ae969b3beed17e0ea31dea56389e3948697694c795ebc7eb73R45.
created_at: 2025-04-25T19:01:23Z
author_association: MEMBER
diff_hunk: @@ -169,46 +132,51 @@ impl Container {
             }
         }
 
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
-
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
-
-            containers.push(container);
-        }
-
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let number_of_containers = self.config.number_of_containers.unwrap_or(1);
+        let mut containers = futures::future::join_all(
+            (0..number_of_containers)
+                .map(|_| self.config.create_and_start_container(&docker, &full_image)),
+        )
+        .await
+        .into_iter()
+        .collect::<Result<VecDeque<_>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self.config.max_lifetime.map(|max_lifetime| {
+            tokio::time::interval(tokio::time::Duration::from_millis(
path: lading/src/generator/container.rs
user: login: L3n41c
body: Fixed in https://github.com/DataDog/lading/pull/1325/commits/fea93299c54706d7b6aac3303ed9c80d793f00b0#diff-884b2b115b4cc2ae969b3beed17e0ea31dea56389e3948697694c795ebc7eb73R171-R172.
created_at: 2025-04-25T19:02:08Z
author_association: MEMBER
diff_hunk: @@ -19,4 +19,5 @@ generator:
       network_disabled: false
       exposed_ports:
         - 5000/tcp
+      max_lifetime: 20
path: examples/lading-container.yaml
user: login: L3n41c
body: Fixed in https://github.com/DataDog/lading/pull/1325/commits/fea93299c54706d7b6aac3303ed9c80d793f00b0#diff-884b2b115b4cc2ae969b3beed17e0ea31dea56389e3948697694c795ebc7eb73R43.
created_at: 2025-04-25T19:02:38Z
author_association: MEMBER
diff_hunk: @@ -37,8 +38,10 @@ pub struct Config {
     pub network_disabled: Option<bool>,
     /// Ports to expose from the container
     pub exposed_ports: Option<Vec<String>>,
+    /// Maximum lifetime of containers before being replaced
+    pub max_lifetime: Option<u64>,
     /// Number of containers to spin up (defaults to 1)
-    pub number_of_containers: Option<u32>,
+    pub number_of_containers: Option<usize>,
path: lading/src/generator/container.rs
user: login: L3n41c
body: Type updated to `std::num::NonZeroU32` in https://github.com/DataDog/lading/pull/1325/commits/fea93299c54706d7b6aac3303ed9c80d793f00b0#diff-884b2b115b4cc2ae969b3beed17e0ea31dea56389e3948697694c795ebc7eb73R45.
created_at: 2025-04-25T20:31:07Z
author_association: MEMBER
diff_hunk: @@ -169,46 +132,51 @@ impl Container {
             }
         }
 
-        let mut containers = Vec::with_capacity(self.number_of_containers);
-
-        for _ in 0..self.number_of_containers {
-            let container_name = format!("lading_container_{}", Uuid::new_v4());
-            debug!("Creating container: {container_name}");
-
-            let container = docker
-                .create_container(
-                    Some(CreateContainerOptions {
-                        name: &container_name,
-                        platform: None,
-                    }),
-                    self.to_container_config(&full_image),
-                )
-                .await?;
-
-            debug!("Created container with id: {id}", id = container.id);
-            for warning in &container.warnings {
-                warn!("Container warning: {warning}");
-            }
-
-            containers.push(container);
-        }
-
-        for container in &containers {
-            docker
-                .start_container(&container.id, None::<StartContainerOptions<String>>)
-                .await?;
-
-            debug!("Started container: {id}", id = container.id);
-        }
+        let number_of_containers = self.config.number_of_containers.unwrap_or(1);
+        let mut containers = futures::future::join_all(
+            (0..number_of_containers)
+                .map(|_| self.config.create_and_start_container(&docker, &full_image)),
+        )
+        .await
+        .into_iter()
+        .collect::<Result<VecDeque<_>, Error>>()?;
 
         // Wait for shutdown signal
         let shutdown_wait = self.shutdown.recv();
         tokio::pin!(shutdown_wait);
-        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
+        let (new_container_tx, mut new_container_rx) = mpsc::channel(32);
+        let mut recreate_interval = self.config.max_lifetime.map(|max_lifetime| {
+            tokio::time::interval(tokio::time::Duration::from_millis(
+                1_000 * max_lifetime / number_of_containers as u64,
+            ))
+        });
+        let mut liveness_interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
         loop {
             tokio::select! {
+                // Destroy and replace containers
+                _ = if let Some(ref mut interval) = recreate_interval { interval.tick() } else { std::future::pending().await } => {
path: lading/src/generator/container.rs
user: login: blt
body: > What would be the most idiomatic way to have a tokio::select! arm triggered by an Option<Interval> ?

Given the context, I think what you have make sense. I may tinker with this area after this merges up for my own curiosity, but I don't have an offhand more idiomatic method. 

> Will the other be properly executed at the next loop iteration?

It will. So long as it doesn't matter which one is executed first, this will work as you expect. 
created_at: 2025-04-28T16:09:33Z
author_association: COLLABORATOR
diff_hunk: @@ -5,11 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+## Added
+- Add a `max_lifetime` parameter to the `container` generator to make it generate
+  a continuous stream of docker containers deletion and re-creation.
 
 ## [0.25.9]
 ## Added
 - Introduce a `container` generator able to generate an arbitrary number
-  of docker containers
+  of docker containers.
 - Compute the Working Set Size of the target thanks to the Linux Idle Page Tracking API.
   And expose it in the new `total_wss_bytes` metric.
path: CHANGELOG.md
user: login: L3n41c
body: ```suggestion
```
created_at: 2025-04-29T10:38:15Z
author_association: MEMBER
diff_hunk: @@ -5,11 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+## Added
+- Add a `max_lifetime` parameter to the `container` generator to make it generate
+  a continuous stream of docker containers deletion and re-creation.
path: CHANGELOG.md
user: login: L3n41c
body: ```suggestion
## [Unreleased]
## Added
- Compute the Working Set Size of the target thanks to the Linux Idle Page Tracking API.
  And expose it in the new `total_wss_bytes` metric.
- Add a `max_lifetime` parameter to the `container` generator to make it generate
  a continuous stream of docker containers deletion and re-creation.
```
created_at: 2025-04-29T10:38:33Z
author_association: MEMBER

PR comments:
user: login: L3n41c
created_at: 2025-04-29T05:20:02Z
body: /merge
author_association: MEMBER
user: login: dd-devflow[bot]
created_at: 2025-04-29T05:20:06Z
body: <!-- "FeedbackWorkflowId":"f4cd8306-8cf8-4b4d-b0e2-d390bd50aeee_36" -->
[View all feedbacks in Devflow UI.](https://mosaic.us1.ddbuild.io/devflow/logs/f4cd8306-8cf8-4b4d-b0e2-d390bd50aeee_36)

`2025-04-29 05:20:06 UTC` :information_source: Start processing command `/merge`

---
`2025-04-29 05:20:09 UTC` :information_source: **MergeQueue:** pull request added to the queue

The expected merge time in `main` is approximately 0s (p90).

---
`2025-04-29 05:20:21 UTC` :x: **MergeQueue:** This merge request has conflicts

This merge request conflicts with another merge request ahead in the queue.

The merge requests in front of this one are:
- [#1322](https://github.com/DataDog/lading/pull/1322) with merge sha [bac30fb](https://github.com/DataDog/lading/commit/bac30fbb1a794e1c1d2b6852d3e205a6408258b0)

author_association: NONE
user: login: L3n41c
created_at: 2025-04-29T05:20:36Z
body: /merge
author_association: MEMBER
user: login: dd-devflow[bot]
created_at: 2025-04-29T05:20:40Z
body: <!-- "FeedbackWorkflowId":"aa4777ff-97d3-42a2-aa99-6e1b2b4b7c68_36" -->
[View all feedbacks in Devflow UI.](https://mosaic.us1.ddbuild.io/devflow/logs/aa4777ff-97d3-42a2-aa99-6e1b2b4b7c68_36)

`2025-04-29 05:20:39 UTC` :information_source: Start processing command `/merge`

---
`2025-04-29 05:20:43 UTC` :information_source: **MergeQueue:** pull request added to the queue

The expected merge time in `main` is approximately 0s (p90).

---
`2025-04-29 05:20:58 UTC` :x: **MergeQueue:** This merge request has conflicts

This merge request conflicts with another merge request ahead in the queue.

The merge requests in front of this one are:
- [#1322](https://github.com/DataDog/lading/pull/1322) with merge sha [5ae94fd](https://github.com/DataDog/lading/commit/5ae94fdd6764dab59accf5c2d0a2ddcfb94d6dae)

author_association: NONE
user: login: L3n41c
created_at: 2025-04-29T10:34:41Z
body: /merge
author_association: MEMBER
user: login: dd-devflow[bot]
created_at: 2025-04-29T10:34:44Z
body: <!-- "FeedbackWorkflowId":"5eaf4893-f96b-40da-b2a4-1f55dcc70edf_36" -->
[View all feedbacks in Devflow UI.](https://mosaic.us1.ddbuild.io/devflow/logs/5eaf4893-f96b-40da-b2a4-1f55dcc70edf_36)

`2025-04-29 10:34:44 UTC` :information_source: Start processing command `/merge`

---
`2025-04-29 10:34:50 UTC` :information_source: **MergeQueue:** waiting for PR to be ready

This merge request is not mergeable yet, because of pending checks/missing approvals. It will be added to the queue as soon as checks pass and/or get approvals.
**Note:** if you pushed new commits since the last approval, you may need additional approval.
You can remove it from the waiting list with `/remove` command.

---
`2025-04-29 11:15:12 UTC` :information_source: **MergeQueue:** merge request added to the queue

The expected merge time in `main` is approximately 7m (p90).

---
`2025-04-29 11:21:31 UTC` :information_source: **MergeQueue:** This merge request was merged

author_association: NONE