This is an automated email from the ASF dual-hosted git repository.

hubcio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new a8dd3a9c9 test(connectors): share one Elasticsearch container across 
tests (#3401)
a8dd3a9c9 is described below

commit a8dd3a9c918e7563c6d18da1d351742aa5ef8aea
Author: Hubert Gruszecki <[email protected]>
AuthorDate: Mon Jun 1 14:42:07 2026 +0200

    test(connectors): share one Elasticsearch container across tests (#3401)
---
 .claude/skills/connector-testing/SKILL.md          | 17 +++++++++++++++
 .config/nextest.toml                               | 15 ++++++++++++--
 .../tests/connectors/fixtures/delta/fixture.rs     |  5 +++--
 .../tests/connectors/fixtures/doris/container.rs   | 10 ++++-----
 .../connectors/fixtures/elasticsearch/container.rs | 20 ++++++++++++------
 .../connectors/fixtures/elasticsearch/sink.rs      | 20 +++++++++++-------
 .../connectors/fixtures/elasticsearch/source.rs    | 20 +++++++++++-------
 .../tests/connectors/fixtures/http/container.rs    |  4 +++-
 .../tests/connectors/fixtures/iceberg/container.rs |  8 +++++---
 .../connectors/fixtures/influxdb/container.rs      |  4 +++-
 .../connectors/fixtures/influxdb/container_v3.rs   |  2 ++
 core/integration/tests/connectors/fixtures/mod.rs  | 18 ++++++++++++++++
 .../tests/connectors/fixtures/mongodb/container.rs |  5 ++++-
 .../connectors/fixtures/postgres/container.rs      | 14 ++++++++-----
 .../connectors/fixtures/quickwit/container.rs      | 10 +++++----
 .../tests/connectors/fixtures/wiremock.rs          |  4 +++-
 justfile                                           | 24 ++++++++++++++++++++++
 17 files changed, 155 insertions(+), 45 deletions(-)

diff --git a/.claude/skills/connector-testing/SKILL.md 
b/.claude/skills/connector-testing/SKILL.md
index ee56ed45c..5aa337907 100644
--- a/.claude/skills/connector-testing/SKILL.md
+++ b/.claude/skills/connector-testing/SKILL.md
@@ -184,6 +184,23 @@ core/integration/tests/connectors/
 └── api/                          HTTP control plane tests
 ```
 
+### Container naming + cleanup
+
+Every test container MUST be named with the `iggy-test-` prefix so a single
+`docker ps -aqf 'name=^iggy-test-'` sweep reaps them (`just 
clean-test-containers`,
+alias `ctc`; the `test`/`nextest` recipes also reap on exit). New fixtures get
+this for free by naming through `fixtures::unique_container_name("<svc>")`, 
which
+appends a uuid for parallel-safe ephemeral containers. Reuse fixtures use a 
fixed
+`iggy-test-<svc>` literal instead, because the stable name is what lets later
+nextest processes attach to the same container via `ReuseDirective::Always`. Do
+not hand-roll a different prefix - the sweep only finds `iggy-test-*`.
+
+As of now only `elasticsearch` and `doris` share a container. Both have a slow
+boot (JVM / Doris FE+BE, tens of seconds), so reusing one across the whole
+regression beats spinning a fresh one per test. Every other fixture boots a
+fresh, ephemeral container per test - cheap enough that the isolation is worth
+more than the reuse.
+
 ### The `#[iggy_harness]` proc macro
 
 Each integration test is annotated with this macro from the `integration` 
crate. It boots an in-process Iggy server (and optionally the connectors 
runtime) for the duration of the test, runs the seeds, then injects a 
`&TestHarness` and your fixture.
diff --git a/.config/nextest.toml b/.config/nextest.toml
index 0f4f9a2c3..3070df8bd 100644
--- a/.config/nextest.toml
+++ b/.config/nextest.toml
@@ -35,10 +35,21 @@ max-threads = 1
 [[profile.default.overrides]]
 filter = 'package(integration) and test(/connectors::doris::/)'
 test-group = "doris"
-slow-timeout = { period = "60s", terminate-after = 8 }
+
+# Elasticsearch tests share one reusable container (fixed name
+# `iggy-test-elasticsearch`, ReuseDirective::Always). Serializing the group
+# lets the first test create it and the rest attach by name, instead of racing
+# to create the same name concurrently (Docker 409 Conflict). Per-test
+# isolation comes from a unique index per fixture, not a fresh container.
+[test-groups.elasticsearch]
+max-threads = 1
+
+[[profile.default.overrides]]
+filter = 'package(integration) and test(/connectors::elasticsearch::/)'
+test-group = "elasticsearch"
 
 [profile.default]
-slow-timeout = { period = "30s", terminate-after = 4 }
+slow-timeout = { period = "60s", terminate-after = 5 }
 
 [profile.ci]
 retries = 3
diff --git a/core/integration/tests/connectors/fixtures/delta/fixture.rs 
b/core/integration/tests/connectors/fixtures/delta/fixture.rs
index 0187e4465..6f9190a16 100644
--- a/core/integration/tests/connectors/fixtures/delta/fixture.rs
+++ b/core/integration/tests/connectors/fixtures/delta/fixture.rs
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+use crate::connectors::fixtures;
 use async_trait::async_trait;
 use deltalake::kernel::{DataType, PrimitiveType, StructField};
 use deltalake::operations::create::CreateBuilder;
@@ -44,7 +45,7 @@ const ENV_SINK_AWS_S3_ENDPOINT_URL: &str =
 const ENV_SINK_AWS_S3_ALLOW_HTTP: &str =
     "IGGY_CONNECTORS_SINK_DELTA_PLUGIN_CONFIG_AWS_S3_ALLOW_HTTP";
 
-const MINIO_IMAGE: &str = "minio/minio";
+const MINIO_IMAGE: &str = "docker.io/minio/minio";
 const MINIO_TAG: &str = "RELEASE.2025-09-07T16-13-09Z";
 const MINIO_PORT: u16 = 9000;
 const MINIO_CONSOLE_PORT: u16 = 9001;
@@ -348,7 +349,7 @@ impl TestFixture for DeltaS3Fixture {
     async fn setup() -> Result<Self, TestBinaryError> {
         let id = Uuid::new_v4();
         let network = format!("iggy-delta-s3-{id}");
-        let minio_name = format!("minio-delta-{id}");
+        let minio_name = fixtures::unique_container_name("minio-delta");
 
         let (minio, minio_endpoint) = Self::start_minio(&network, 
&minio_name).await?;
         Self::create_bucket(&minio_endpoint).await?;
diff --git a/core/integration/tests/connectors/fixtures/doris/container.rs 
b/core/integration/tests/connectors/fixtures/doris/container.rs
index e18e53a60..0a1fbe141 100644
--- a/core/integration/tests/connectors/fixtures/doris/container.rs
+++ b/core/integration/tests/connectors/fixtures/doris/container.rs
@@ -19,7 +19,7 @@
 
 // Doris fixture. One Doris container is shared across every doris test in this
 // CI job (or local `cargo test` session) via testcontainers' 
reusable-containers
-// feature: the container is named `iggy-doris-test` and marked
+// feature: the container is named `iggy-test-doris` and marked
 // `ReuseDirective::Always`, so the first test creates it and every subsequent
 // test (in this or any other test process on the same Docker daemon) attaches
 // to it. This is what makes `cargo test` and CI follow the same path — the old
@@ -31,7 +31,7 @@
 //
 // The container outlives the test session by design (that's what enables
 // reuse). CI runners are ephemeral so it dies with them; locally, `docker rm
-// -f iggy-doris-test` forces a fresh boot.
+// -f iggy-test-doris` forces a fresh boot.
 //
 // Two host-level prerequisites cannot live inside the container:
 //   * `vm.max_map_count >= 2_000_000` — Doris 4.0.3's `start_be.sh` hard-exits
@@ -57,7 +57,7 @@ use tokio::time::sleep;
 use tracing::info;
 use uuid::Uuid;
 
-const DORIS_IMAGE: &str = "apache/doris";
+const DORIS_IMAGE: &str = "docker.io/apache/doris";
 // Apache's maintained single-container line is now tagged `<version>-all` /
 // `<version>-all-slim` (the old one-off `doris-all-in-one-2.1.0` was pushed 
once
 // in 2024 and never refreshed). `-slim` is the smaller base, so it pulls 
faster
@@ -66,7 +66,7 @@ const DORIS_TAG: &str = "4.0.3-all-slim";
 // Fixed name + `ReuseDirective::Always` is what makes the container survive
 // across nextest's per-test processes. Stable name means every test process
 // inspecting the Docker daemon finds the same one.
-const DORIS_CONTAINER_NAME: &str = "iggy-doris-test";
+const DORIS_CONTAINER_NAME: &str = "iggy-test-doris";
 const FE_HTTP_PORT: u16 = 8030;
 const FE_MYSQL_PORT: u16 = 9030;
 const BE_HTTP_PORT: u16 = 8040;
@@ -232,7 +232,7 @@ impl DorisContainer {
         //
         // `with_container_name` + `with_reuse(Always)` is what makes the
         // container survive across nextest's per-test processes: the first
-        // test creates `iggy-doris-test`, every later test (in any process)
+        // test creates `iggy-test-doris`, every later test (in any process)
         // attaches to it. The 1:1 BE port is therefore held continuously by
         // one container, never racing with itself across container restarts.
         let container = GenericImage::new(DORIS_IMAGE, DORIS_TAG)
diff --git 
a/core/integration/tests/connectors/fixtures/elasticsearch/container.rs 
b/core/integration/tests/connectors/fixtures/elasticsearch/container.rs
index 3eacc3a0a..32d15f069 100644
--- a/core/integration/tests/connectors/fixtures/elasticsearch/container.rs
+++ b/core/integration/tests/connectors/fixtures/elasticsearch/container.rs
@@ -25,14 +25,20 @@ use serde::Deserialize;
 use testcontainers_modules::testcontainers::core::wait::HttpWaitStrategy;
 use testcontainers_modules::testcontainers::core::{IntoContainerPort, WaitFor};
 use testcontainers_modules::testcontainers::runners::AsyncRunner;
-use testcontainers_modules::testcontainers::{ContainerAsync, GenericImage, 
ImageExt};
+use testcontainers_modules::testcontainers::{
+    ContainerAsync, GenericImage, ImageExt, ReuseDirective,
+};
 use tracing::info;
-use uuid::Uuid;
 
-const ELASTICSEARCH_IMAGE: &str = "elasticsearch";
+const ELASTICSEARCH_IMAGE: &str = "docker.io/library/elasticsearch";
 const ELASTICSEARCH_TAG: &str = "9.3.0";
 const ELASTICSEARCH_PORT: u16 = 9200;
 const ELASTICSEARCH_HEALTH_ENDPOINT: &str = "/_cluster/health";
+// Fixed name + ReuseDirective::Always shares one container across nextest's
+// per-test processes: the first test creates it, every later test attaches by
+// name. Per-test isolation comes from a unique index per fixture, not a fresh
+// container.
+const ELASTICSEARCH_CONTAINER_NAME: &str = "iggy-test-elasticsearch";
 
 pub const DEFAULT_TEST_STREAM: &str = "test_stream";
 pub const DEFAULT_TEST_TOPIC: &str = "test_topic";
@@ -88,6 +94,8 @@ pub struct ElasticsearchHit {
 }
 
 pub struct ElasticsearchContainer {
+    // Held so testcontainers' Drop runs on test exit; ReuseDirective::Always
+    // makes that Drop leave the container running for the next test to attach.
     #[allow(dead_code)]
     container: ContainerAsync<GenericImage>,
     pub base_url: String,
@@ -95,8 +103,6 @@ pub struct ElasticsearchContainer {
 
 impl ElasticsearchContainer {
     pub async fn start() -> Result<Self, TestBinaryError> {
-        let unique_network = format!("iggy-elasticsearch-source-{}", 
Uuid::new_v4());
-
         let container = GenericImage::new(ELASTICSEARCH_IMAGE, 
ELASTICSEARCH_TAG)
             .with_exposed_port(ELASTICSEARCH_PORT.tcp())
             .with_wait_for(WaitFor::http(
@@ -104,11 +110,13 @@ impl ElasticsearchContainer {
                     .with_port(ELASTICSEARCH_PORT.tcp())
                     .with_expected_status_code(200u16),
             ))
-            .with_network(unique_network)
+            .with_startup_timeout(std::time::Duration::from_secs(120))
             .with_env_var("discovery.type", "single-node")
             .with_env_var("xpack.security.enabled", "false")
             .with_env_var("ES_JAVA_OPTS", "-Xms512m -Xmx512m")
             .with_mapped_port(0, ELASTICSEARCH_PORT.tcp())
+            .with_container_name(ELASTICSEARCH_CONTAINER_NAME)
+            .with_reuse(ReuseDirective::Always)
             .start()
             .await
             .map_err(|e| TestBinaryError::FixtureSetup {
diff --git a/core/integration/tests/connectors/fixtures/elasticsearch/sink.rs 
b/core/integration/tests/connectors/fixtures/elasticsearch/sink.rs
index 334dc72f1..bce21d5ea 100644
--- a/core/integration/tests/connectors/fixtures/elasticsearch/sink.rs
+++ b/core/integration/tests/connectors/fixtures/elasticsearch/sink.rs
@@ -30,14 +30,18 @@ use std::collections::HashMap;
 use std::time::Duration;
 use tokio::time::sleep;
 use tracing::info;
+use uuid::Uuid;
 
-const SINK_INDEX: &str = "iggy_messages";
+const SINK_INDEX_PREFIX: &str = "iggy_messages";
 const POLL_ATTEMPTS: usize = 100;
 const POLL_INTERVAL_MS: u64 = 50;
 
 pub struct ElasticsearchSinkFixture {
     container: ElasticsearchContainer,
     http_client: HttpClient,
+    // Unique per fixture so tests sharing one container never collide on the
+    // same index. The connector writes here via ENV_SINK_INDEX.
+    index: String,
 }
 
 impl ElasticsearchOps for ElasticsearchSinkFixture {
@@ -52,7 +56,7 @@ impl ElasticsearchOps for ElasticsearchSinkFixture {
 
 impl ElasticsearchSinkFixture {
     pub async fn get_document_count(&self) -> Result<usize, TestBinaryError> {
-        self.count_documents(SINK_INDEX).await
+        self.count_documents(&self.index).await
     }
 
     pub async fn wait_for_documents(
@@ -60,7 +64,7 @@ impl ElasticsearchSinkFixture {
         expected_count: usize,
     ) -> Result<usize, TestBinaryError> {
         for _ in 0..POLL_ATTEMPTS {
-            match self.count_documents(SINK_INDEX).await {
+            match self.count_documents(&self.index).await {
                 Ok(count) if count >= expected_count => {
                     info!("Found {count} documents in Elasticsearch (expected 
{expected_count})");
                     return Ok(count);
@@ -71,7 +75,7 @@ impl ElasticsearchSinkFixture {
             sleep(Duration::from_millis(POLL_INTERVAL_MS)).await;
         }
 
-        let final_count = self.count_documents(SINK_INDEX).await.unwrap_or(0);
+        let final_count = self.count_documents(&self.index).await.unwrap_or(0);
         Err(TestBinaryError::InvalidState {
             message: format!(
                 "Expected at least {expected_count} documents, found 
{final_count} after {POLL_ATTEMPTS} attempts"
@@ -80,11 +84,11 @@ impl ElasticsearchSinkFixture {
     }
 
     pub async fn search_documents(&self) -> 
Result<ElasticsearchSearchResponse, TestBinaryError> {
-        self.search_all(SINK_INDEX).await
+        self.search_all(&self.index).await
     }
 
     pub async fn refresh_index(&self) -> Result<(), TestBinaryError> {
-        ElasticsearchOps::refresh_index(self, SINK_INDEX).await
+        ElasticsearchOps::refresh_index(self, &self.index).await
     }
 }
 
@@ -93,19 +97,21 @@ impl TestFixture for ElasticsearchSinkFixture {
     async fn setup() -> Result<Self, TestBinaryError> {
         let container = ElasticsearchContainer::start().await?;
         let http_client = create_http_client();
+        let index = format!("{SINK_INDEX_PREFIX}_{}", Uuid::new_v4().simple());
 
         // Container startup already waits for /_cluster/health to return 200
         // via HttpWaitStrategy, so no additional health check is needed.
         Ok(Self {
             container,
             http_client,
+            index,
         })
     }
 
     fn connectors_runtime_envs(&self) -> HashMap<String, String> {
         let mut envs = HashMap::new();
         envs.insert(ENV_SINK_URL.to_string(), self.container.base_url.clone());
-        envs.insert(ENV_SINK_INDEX.to_string(), SINK_INDEX.to_string());
+        envs.insert(ENV_SINK_INDEX.to_string(), self.index.clone());
         envs.insert(
             ENV_SINK_STREAMS_0_STREAM.to_string(),
             DEFAULT_TEST_STREAM.to_string(),
diff --git a/core/integration/tests/connectors/fixtures/elasticsearch/source.rs 
b/core/integration/tests/connectors/fixtures/elasticsearch/source.rs
index 16bdaaeae..8f8bb4a1a 100644
--- a/core/integration/tests/connectors/fixtures/elasticsearch/source.rs
+++ b/core/integration/tests/connectors/fixtures/elasticsearch/source.rs
@@ -28,13 +28,17 @@ use iggy_common::IggyTimestamp;
 use integration::harness::{TestBinaryError, TestFixture};
 use reqwest_middleware::ClientWithMiddleware as HttpClient;
 use std::collections::HashMap;
+use uuid::Uuid;
 
-const TEST_INDEX: &str = "test_documents";
+const TEST_INDEX_PREFIX: &str = "test_documents";
 
 /// Elasticsearch source fixture for basic document polling.
 pub struct ElasticsearchSourceFixture {
     container: ElasticsearchContainer,
     http_client: HttpClient,
+    // Unique per fixture so tests sharing one container never collide on the
+    // same index. The connector reads from here via ENV_SOURCE_INDEX.
+    index: String,
 }
 
 impl ElasticsearchOps for ElasticsearchSourceFixture {
@@ -50,11 +54,11 @@ impl ElasticsearchOps for ElasticsearchSourceFixture {
 impl ElasticsearchSourceFixture {
     #[allow(dead_code)]
     pub fn index_name(&self) -> &str {
-        TEST_INDEX
+        &self.index
     }
 
     pub async fn setup_index(&self) -> Result<(), TestBinaryError> {
-        self.create_index(TEST_INDEX).await
+        self.create_index(&self.index).await
     }
 
     pub async fn insert_document(
@@ -70,7 +74,7 @@ impl ElasticsearchSourceFixture {
             "value": value,
             "timestamp": timestamp
         });
-        self.index_document(TEST_INDEX, &doc_id.to_string(), &document)
+        self.index_document(&self.index, &doc_id.to_string(), &document)
             .await
     }
 
@@ -84,11 +88,11 @@ impl ElasticsearchSourceFixture {
     }
 
     pub async fn get_document_count(&self) -> Result<usize, TestBinaryError> {
-        self.count_documents(TEST_INDEX).await
+        self.count_documents(&self.index).await
     }
 
     pub async fn refresh_index(&self) -> Result<(), TestBinaryError> {
-        ElasticsearchOps::refresh_index(self, TEST_INDEX).await
+        ElasticsearchOps::refresh_index(self, &self.index).await
     }
 }
 
@@ -97,19 +101,21 @@ impl TestFixture for ElasticsearchSourceFixture {
     async fn setup() -> Result<Self, TestBinaryError> {
         let container = ElasticsearchContainer::start().await?;
         let http_client = create_http_client();
+        let index = format!("{TEST_INDEX_PREFIX}_{}", Uuid::new_v4().simple());
 
         // Container startup already waits for /_cluster/health to return 200
         // via HttpWaitStrategy, so no additional health check is needed.
         Ok(Self {
             container,
             http_client,
+            index,
         })
     }
 
     fn connectors_runtime_envs(&self) -> HashMap<String, String> {
         let mut envs = HashMap::new();
         envs.insert(ENV_SOURCE_URL.to_string(), 
self.container.base_url.clone());
-        envs.insert(ENV_SOURCE_INDEX.to_string(), TEST_INDEX.to_string());
+        envs.insert(ENV_SOURCE_INDEX.to_string(), self.index.clone());
         envs.insert(ENV_SOURCE_POLLING_INTERVAL.to_string(), 
"100ms".to_string());
         envs.insert(ENV_SOURCE_BATCH_SIZE.to_string(), "100".to_string());
         envs.insert(
diff --git a/core/integration/tests/connectors/fixtures/http/container.rs 
b/core/integration/tests/connectors/fixtures/http/container.rs
index 1f5204871..669166aff 100644
--- a/core/integration/tests/connectors/fixtures/http/container.rs
+++ b/core/integration/tests/connectors/fixtures/http/container.rs
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+use crate::connectors::fixtures;
 use integration::harness::TestBinaryError;
 use std::time::Duration;
 use testcontainers_modules::testcontainers::core::WaitFor::Healthcheck;
@@ -27,7 +28,7 @@ use testcontainers_modules::testcontainers::{ContainerAsync, 
GenericImage, Image
 use tokio::time::sleep;
 use tracing::info;
 
-const WIREMOCK_IMAGE: &str = "wiremock/wiremock";
+const WIREMOCK_IMAGE: &str = "docker.io/wiremock/wiremock";
 const WIREMOCK_TAG: &str = "3.13.2";
 const WIREMOCK_PORT: u16 = 8080;
 
@@ -86,6 +87,7 @@ impl HttpSinkWireMockContainer {
                     .to_string(),
                 "/home/wiremock/mappings",
             ))
+            
.with_container_name(fixtures::unique_container_name("wiremock-http"))
             .start()
             .await
             .map_err(|e| TestBinaryError::FixtureSetup {
diff --git a/core/integration/tests/connectors/fixtures/iceberg/container.rs 
b/core/integration/tests/connectors/fixtures/iceberg/container.rs
index c0bcc5c13..c4ad10f40 100644
--- a/core/integration/tests/connectors/fixtures/iceberg/container.rs
+++ b/core/integration/tests/connectors/fixtures/iceberg/container.rs
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+use crate::connectors::fixtures;
 use async_trait::async_trait;
 use integration::harness::{TestBinaryError, TestFixture};
 use reqwest_middleware::ClientWithMiddleware as HttpClient;
@@ -30,11 +31,11 @@ use 
testcontainers_modules::testcontainers::{ContainerAsync, GenericImage, Image
 use tracing::info;
 use uuid::Uuid;
 
-const MINIO_IMAGE: &str = "minio/minio";
+const MINIO_IMAGE: &str = "docker.io/minio/minio";
 const MINIO_TAG: &str = "RELEASE.2025-09-07T16-13-09Z";
 const MINIO_PORT: u16 = 9000;
 const MINIO_CONSOLE_PORT: u16 = 9001;
-const ICEBERG_REST_IMAGE: &str = "apache/iceberg-rest-fixture";
+const ICEBERG_REST_IMAGE: &str = "docker.io/apache/iceberg-rest-fixture";
 const ICEBERG_REST_TAG: &str = "latest";
 const ICEBERG_REST_PORT: u16 = 8181;
 
@@ -150,6 +151,7 @@ impl IcebergRestContainer {
             .with_env_var("AWS_ACCESS_KEY_ID", MINIO_ACCESS_KEY)
             .with_env_var("AWS_SECRET_ACCESS_KEY", MINIO_SECRET_KEY)
             .with_mapped_port(0, ICEBERG_REST_PORT.tcp())
+            
.with_container_name(fixtures::unique_container_name("iceberg-rest"))
             .start()
             .await
             .map_err(|error| TestBinaryError::FixtureSetup {
@@ -448,7 +450,7 @@ impl TestFixture for IcebergFixture {
     async fn setup() -> Result<Self, TestBinaryError> {
         let id = Uuid::new_v4();
         let network = format!("iggy-iceberg-{id}");
-        let minio_name = format!("minio-{id}");
+        let minio_name = fixtures::unique_container_name("minio-iceberg");
 
         let minio = MinioContainer::start(&network, &minio_name).await?;
 
diff --git a/core/integration/tests/connectors/fixtures/influxdb/container.rs 
b/core/integration/tests/connectors/fixtures/influxdb/container.rs
index dcf707c52..914430bb3 100644
--- a/core/integration/tests/connectors/fixtures/influxdb/container.rs
+++ b/core/integration/tests/connectors/fixtures/influxdb/container.rs
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+use crate::connectors::fixtures;
 use integration::harness::TestBinaryError;
 use reqwest_middleware::ClientWithMiddleware as HttpClient;
 use reqwest_retry::RetryTransientMiddleware;
@@ -27,7 +28,7 @@ use 
testcontainers_modules::testcontainers::runners::AsyncRunner;
 use testcontainers_modules::testcontainers::{ContainerAsync, GenericImage, 
ImageExt};
 use tracing::info;
 
-const INFLUXDB_IMAGE: &str = "influxdb";
+const INFLUXDB_IMAGE: &str = "docker.io/library/influxdb";
 const INFLUXDB_TAG: &str = "2.7-alpine";
 const INFLUXDB_PORT: u16 = 8086;
 
@@ -117,6 +118,7 @@ impl InfluxDbContainer {
                 .with_env_var("DOCKER_INFLUXDB_INIT_ORG", INFLUXDB_ORG)
                 .with_env_var("DOCKER_INFLUXDB_INIT_BUCKET", INFLUXDB_BUCKET)
                 .with_env_var("DOCKER_INFLUXDB_INIT_ADMIN_TOKEN", 
INFLUXDB_TOKEN)
+                
.with_container_name(fixtures::unique_container_name("influxdb"))
                 .start()
                 .await
                 .map_err(|e| TestBinaryError::FixtureSetup {
diff --git 
a/core/integration/tests/connectors/fixtures/influxdb/container_v3.rs 
b/core/integration/tests/connectors/fixtures/influxdb/container_v3.rs
index 138ccdef9..dcbe63402 100644
--- a/core/integration/tests/connectors/fixtures/influxdb/container_v3.rs
+++ b/core/integration/tests/connectors/fixtures/influxdb/container_v3.rs
@@ -26,6 +26,7 @@
 //! - Query: `POST /api/v3/query_sql` with `{"db":…,"q":…,"format":"jsonl"}`
 //! - Auth: `Authorization: Bearer {token}`
 
+use crate::connectors::fixtures;
 use integration::harness::TestBinaryError;
 use reqwest_middleware::ClientWithMiddleware as HttpClient;
 use testcontainers_modules::testcontainers::core::{IntoContainerPort, WaitFor};
@@ -115,6 +116,7 @@ impl InfluxDb3Container {
                     "memory",
                     "--without-auth",
                 ])
+                
.with_container_name(fixtures::unique_container_name("influxdb3"))
                 .start()
                 .await
                 .map_err(|e| TestBinaryError::FixtureSetup {
diff --git a/core/integration/tests/connectors/fixtures/mod.rs 
b/core/integration/tests/connectors/fixtures/mod.rs
index 2951cacbe..616c3a557 100644
--- a/core/integration/tests/connectors/fixtures/mod.rs
+++ b/core/integration/tests/connectors/fixtures/mod.rs
@@ -17,6 +17,8 @@
  * under the License.
  */
 
+use uuid::Uuid;
+
 mod delta;
 mod doris;
 mod elasticsearch;
@@ -28,6 +30,22 @@ mod postgres;
 mod quickwit;
 mod wiremock;
 
+/// Prefix on every test container name so `just clean-test-containers` reaps
+/// them all with one `name=^iggy-test-` filter. A new fixture only has to use
+/// `unique_container_name` (or a fixed `iggy-test-<svc>` for reuse containers)
+/// to be covered.
+pub(crate) const TEST_CONTAINER_PREFIX: &str = "iggy-test-";
+
+/// Unique per-test container name for ephemeral fixtures. Reuse fixtures
+/// (elasticsearch, doris) use a fixed `iggy-test-<svc>` literal instead, since
+/// the stable name is what lets later test processes attach to the same one.
+pub(crate) fn unique_container_name(service: &str) -> String {
+    format!(
+        "{TEST_CONTAINER_PREFIX}{service}-{}",
+        Uuid::new_v4().simple()
+    )
+}
+
 pub use delta::{DeltaFixture, DeltaS3Fixture};
 pub use doris::{
     DorisOps, DorisSinkColumnsMappingFixture, DorisSinkFixture, 
DorisSinkMaxFilterRatioFixture,
diff --git a/core/integration/tests/connectors/fixtures/mongodb/container.rs 
b/core/integration/tests/connectors/fixtures/mongodb/container.rs
index 4c2b5177d..f58bd3e27 100644
--- a/core/integration/tests/connectors/fixtures/mongodb/container.rs
+++ b/core/integration/tests/connectors/fixtures/mongodb/container.rs
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+use crate::connectors::fixtures;
 use integration::harness::TestBinaryError;
 use mongodb::{Client, bson::doc, options::ClientOptions};
 use std::time::Duration;
@@ -26,7 +27,7 @@ use testcontainers_modules::testcontainers::{ContainerAsync, 
GenericImage, Image
 use tokio::time::sleep;
 use tracing::info;
 
-const MONGODB_IMAGE: &str = "mongo";
+const MONGODB_IMAGE: &str = "docker.io/library/mongo";
 const MONGODB_TAG: &str = "7";
 const MONGODB_PORT: u16 = 27017;
 const MONGODB_READY_MSG: &str = "Waiting for connections";
@@ -80,6 +81,7 @@ impl MongoDbContainer {
             .with_exposed_port(MONGODB_PORT.tcp())
             .with_wait_for(WaitFor::message_on_stdout(MONGODB_READY_MSG))
             .with_mapped_port(0, MONGODB_PORT.tcp())
+            .with_container_name(fixtures::unique_container_name("mongodb"))
             .start()
             .await
             .map_err(|e| TestBinaryError::FixtureSetup {
@@ -121,6 +123,7 @@ impl MongoDbContainer {
             .with_exposed_port(MONGODB_PORT.tcp())
             .with_wait_for(WaitFor::message_on_stdout(MONGODB_READY_MSG))
             .with_mapped_port(0, MONGODB_PORT.tcp())
+            .with_container_name(fixtures::unique_container_name("mongodb-rs"))
             .with_cmd(["--replSet", MONGODB_REPLICA_SET_NAME, 
"--bind_ip_all"]);
 
         if enable_test_commands {
diff --git a/core/integration/tests/connectors/fixtures/postgres/container.rs 
b/core/integration/tests/connectors/fixtures/postgres/container.rs
index 51478810f..b2c5ae288 100644
--- a/core/integration/tests/connectors/fixtures/postgres/container.rs
+++ b/core/integration/tests/connectors/fixtures/postgres/container.rs
@@ -20,9 +20,11 @@
 use integration::harness::TestBinaryError;
 use sqlx::postgres::PgPoolOptions;
 use sqlx::{Pool, Postgres};
+
+use crate::connectors::fixtures;
 use testcontainers_modules::{
     postgres,
-    testcontainers::{ContainerAsync, runners::AsyncRunner},
+    testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner},
 };
 
 pub(super) const POSTGRES_PORT: u16 = 5432;
@@ -128,12 +130,14 @@ pub struct PostgresContainer {
 
 impl PostgresContainer {
     pub(super) async fn start() -> Result<Self, TestBinaryError> {
-        let container = 
postgres::Postgres::default().start().await.map_err(|e| {
-            TestBinaryError::FixtureSetup {
+        let container = postgres::Postgres::default()
+            .with_container_name(fixtures::unique_container_name("postgres"))
+            .start()
+            .await
+            .map_err(|e| TestBinaryError::FixtureSetup {
                 fixture_type: "PostgresContainer".to_string(),
                 message: format!("Failed to start container: {e}"),
-            }
-        })?;
+            })?;
 
         let host_port = container
             .get_host_port_ipv4(POSTGRES_PORT)
diff --git a/core/integration/tests/connectors/fixtures/quickwit/container.rs 
b/core/integration/tests/connectors/fixtures/quickwit/container.rs
index 9d062937c..de0760b83 100644
--- a/core/integration/tests/connectors/fixtures/quickwit/container.rs
+++ b/core/integration/tests/connectors/fixtures/quickwit/container.rs
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+use crate::connectors::fixtures;
 use async_trait::async_trait;
 use integration::harness::{TestBinaryError, TestFixture, seeds};
 use reqwest_middleware::ClientWithMiddleware as HttpClient;
@@ -33,10 +34,10 @@ use tokio::time::sleep;
 use tracing::info;
 use uuid::Uuid;
 
-const DEFAULT_POLL_ATTEMPTS: usize = 100;
-const DEFAULT_POLL_INTERVAL_MS: u64 = 50;
+const DEFAULT_POLL_ATTEMPTS: usize = 600;
+const DEFAULT_POLL_INTERVAL_MS: u64 = 100;
 
-const QUICKWIT_IMAGE: &str = "quickwit/quickwit";
+const QUICKWIT_IMAGE: &str = "docker.io/quickwit/quickwit";
 const QUICKWIT_TAG: &str = "0.8.2";
 const QUICKWIT_PORT: u16 = 7280;
 const QUICKWIT_LISTEN_ADDRESS: &str = "0.0.0.0";
@@ -78,6 +79,7 @@ impl QuickwitContainer {
             .with_cmd(["run"])
             .with_env_var("QW_LISTEN_ADDRESS", QUICKWIT_LISTEN_ADDRESS)
             .with_mapped_port(0, QUICKWIT_PORT.tcp())
+            .with_container_name(fixtures::unique_container_name("quickwit"))
             .start()
             .await
             .map_err(|e| TestBinaryError::FixtureSetup {
@@ -253,7 +255,7 @@ pub trait QuickwitOps: Sync {
                         return Ok(search);
                     }
                 }
-                sleep(Duration::from_millis(DEFAULT_POLL_INTERVAL_MS / 
5)).await;
+                sleep(Duration::from_millis(DEFAULT_POLL_INTERVAL_MS)).await;
             }
             Err(TestBinaryError::InvalidState {
                 message: format!(
diff --git a/core/integration/tests/connectors/fixtures/wiremock.rs 
b/core/integration/tests/connectors/fixtures/wiremock.rs
index ceda81949..75e886e97 100644
--- a/core/integration/tests/connectors/fixtures/wiremock.rs
+++ b/core/integration/tests/connectors/fixtures/wiremock.rs
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+use crate::connectors::fixtures;
 use async_trait::async_trait;
 use integration::harness::{TestBinaryError, TestFixture};
 use std::collections::HashMap;
@@ -26,7 +27,7 @@ use 
testcontainers_modules::testcontainers::core::{IntoContainerPort, Mount};
 use testcontainers_modules::testcontainers::runners::AsyncRunner;
 use testcontainers_modules::testcontainers::{ContainerAsync, GenericImage, 
ImageExt};
 
-const WIREMOCK_IMAGE: &str = "wiremock/wiremock";
+const WIREMOCK_IMAGE: &str = "docker.io/wiremock/wiremock";
 const WIREMOCK_TAG: &str = "3.13.2";
 const WIREMOCK_PORT: u16 = 8080;
 
@@ -62,6 +63,7 @@ impl WireMockContainer {
                     .to_string(),
                 "/home/wiremock/__files",
             ))
+            .with_container_name(fixtures::unique_container_name("wiremock"))
             .start()
             .await
             .map_err(|e| TestBinaryError::FixtureSetup {
diff --git a/justfile b/justfile
index 7b68d0c4d..3ad75a6fd 100644
--- a/justfile
+++ b/justfile
@@ -32,22 +32,46 @@ alias pcs := profile-cpu-server
 alias pcc := profile-cpu-client
 alias pis := profile-io-server
 alias pic := profile-io-client
+alias ctc := clean-test-containers
+
+# Every test container is named `iggy-test-<service>[-<uuid>]` (see
+# fixtures::unique_container_name). Reuse fixtures (elasticsearch, doris) keep
+# their container running, and a crashed run can orphan any fixture's 
container,
+# so the recipes below reap everything matching the prefix on exit.
+reap_test_containers := "docker ps -aqf 'name=^iggy-test-' | xargs -r docker 
rm -f >/dev/null 2>&1 || true"
 
 build:
   cargo build
 
 test: build
+  #!/usr/bin/env bash
+  set -euo pipefail
+  trap "{{reap_test_containers}}" EXIT
   cargo test
 
 tests TEST: build
+  #!/usr/bin/env bash
+  set -euo pipefail
+  trap "{{reap_test_containers}}" EXIT
   cargo test {{TEST}}
 
 nextest: build
+  #!/usr/bin/env bash
+  set -euo pipefail
+  trap "{{reap_test_containers}}" EXIT
   cargo nextest run
 
 nextests TEST: build
+  #!/usr/bin/env bash
+  set -euo pipefail
+  trap "{{reap_test_containers}}" EXIT
   cargo nextest run --nocapture -- {{TEST}}
 
+# Force-remove all `iggy-test-*` containers on demand. Useful after running
+# `cargo nextest run` directly (outside the recipes above).
+clean-test-containers:
+  {{reap_test_containers}}
+
 # Run Miri (UB detector) on the unsafe-heavy crates that don't pull
 # tokio/compio. Mirrors the `miri` task in CI. Pinned to the same nightly
 # as `.github/actions/rust/pre-merge/action.yml` so local runs don't drift

Reply via email to