-
Notifications
You must be signed in to change notification settings - Fork 0
fix(infra): harden Testcontainers shutdown hooks with safeStop #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.test.context.DynamicPropertySource; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.containers.KafkaContainer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.containers.PostgreSQLContainer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.lifecycle.Startable; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.utility.DockerImageName; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @SuppressWarnings("resource") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| POSTGRES.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| KAFKA.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| POSTGRES.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| KAFKA.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (RuntimeException ex) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| safeStop(KAFKA); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| safeStop(POSTGRES); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw ex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| KAFKA.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| POSTGRES.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| safeStop(KAFKA); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| safeStop(POSTGRES); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, "testcontainers-shutdown")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
31
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Register the shutdown hook before startup to guarantee last-resort cleanup. At Line 38, rethrow exits the static block, so Lines 40-43 never execute. If a stop attempt in the catch path fails, there is no shutdown hook fallback and containers can leak for the JVM lifetime. Proposed fix static {
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ safeStop(KAFKA);
+ safeStop(POSTGRES);
+ }, "testcontainers-shutdown"));
+
try {
POSTGRES.start();
KAFKA.start();
} catch (RuntimeException ex) {
safeStop(KAFKA);
safeStop(POSTGRES);
throw ex;
}
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- safeStop(KAFKA);
- safeStop(POSTGRES);
- }, "testcontainers-shutdown"));
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static void safeStop(Startable container) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (container != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| container.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception ignored) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // best-effort cleanup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Avoid fully silent stop failures; emit minimal infra diagnostics. Suppressing cleanup exceptions is fine, but fully silent failures make container lifecycle flakiness hard to diagnose. Log container type + exception class/message (no payload/PII). 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Autowired | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| import org.springframework.test.context.DynamicPropertySource; | ||
| import org.testcontainers.containers.KafkaContainer; | ||
| import org.testcontainers.containers.PostgreSQLContainer; | ||
| import org.testcontainers.lifecycle.Startable; | ||
| import org.testcontainers.utility.DockerImageName; | ||
|
|
||
| @SuppressWarnings("resource") | ||
|
|
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest { | |
| new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0")); | ||
|
|
||
| static { | ||
| POSTGRES.start(); | ||
| KAFKA.start(); | ||
| try { | ||
| POSTGRES.start(); | ||
| KAFKA.start(); | ||
| } catch (RuntimeException ex) { | ||
| safeStop(KAFKA); | ||
| safeStop(POSTGRES); | ||
| throw ex; | ||
| } | ||
| Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||
| KAFKA.stop(); | ||
| POSTGRES.stop(); | ||
| })); | ||
| safeStop(KAFKA); | ||
| safeStop(POSTGRES); | ||
| }, "testcontainers-shutdown")); | ||
| } | ||
|
|
||
| private static void safeStop(Startable container) { | ||
| try { | ||
| if (container != null) { | ||
| container.stop(); | ||
| } | ||
| } catch (Exception ignored) { | ||
| // best-effort cleanup | ||
| } | ||
|
Comment on lines
+46
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not fully suppress stop failures in Line 51 currently discards cleanup exceptions entirely; when container teardown fails in CI, this hides the root cause and makes resource-leak triage difficult. Log at least a WARN with container type and exception. Suggested patch+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.testcontainers.lifecycle.Startable;
@@
public abstract class AbstractIntegrationTest {
+ private static final Logger log = LoggerFactory.getLogger(AbstractIntegrationTest.class);
@@
private static void safeStop(Startable container) {
try {
if (container != null) {
container.stop();
}
- } catch (Exception ignored) {
- // best-effort cleanup
+ } catch (Exception ex) {
+ log.warn("Best-effort Testcontainers cleanup failed for {}",
+ container != null ? container.getClass().getSimpleName() : "unknown", ex);
}
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @Autowired | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |||||||||||||||||||||||||||||||||||||||||
| import org.springframework.test.context.DynamicPropertySource; | ||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.containers.KafkaContainer; | ||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.containers.PostgreSQLContainer; | ||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.lifecycle.Startable; | ||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.utility.DockerImageName; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @SuppressWarnings("resource") | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest { | |||||||||||||||||||||||||||||||||||||||||
| new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0")); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| static { | ||||||||||||||||||||||||||||||||||||||||||
| POSTGRES.start(); | ||||||||||||||||||||||||||||||||||||||||||
| KAFKA.start(); | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| POSTGRES.start(); | ||||||||||||||||||||||||||||||||||||||||||
| KAFKA.start(); | ||||||||||||||||||||||||||||||||||||||||||
| } catch (RuntimeException ex) { | ||||||||||||||||||||||||||||||||||||||||||
| safeStop(KAFKA); | ||||||||||||||||||||||||||||||||||||||||||
| safeStop(POSTGRES); | ||||||||||||||||||||||||||||||||||||||||||
| throw ex; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||||||||||||||||||||||||||||||||||||||||||
| KAFKA.stop(); | ||||||||||||||||||||||||||||||||||||||||||
| POSTGRES.stop(); | ||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||
| safeStop(KAFKA); | ||||||||||||||||||||||||||||||||||||||||||
| safeStop(POSTGRES); | ||||||||||||||||||||||||||||||||||||||||||
| }, "testcontainers-shutdown")); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private static void safeStop(Startable container) { | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| if (container != null) { | ||||||||||||||||||||||||||||||||||||||||||
| container.stop(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception ignored) { | ||||||||||||||||||||||||||||||||||||||||||
| // best-effort cleanup | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider logging suppressed exceptions. Silently swallowing exceptions in ♻️ Proposed improvement+ private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(AbstractIntegrationTest.class);
+
private static void safeStop(Startable container) {
try {
if (container != null) {
container.stop();
}
- } catch (Exception ignored) {
- // best-effort cleanup
+ } catch (Exception ex) {
+ LOG.warn("Best-effort container stop failed: {}", ex.getMessage());
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @Autowired | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| import org.springframework.test.context.DynamicPropertySource; | ||
| import org.testcontainers.containers.KafkaContainer; | ||
| import org.testcontainers.containers.PostgreSQLContainer; | ||
| import org.testcontainers.lifecycle.Startable; | ||
| import org.testcontainers.utility.DockerImageName; | ||
|
|
||
| @SuppressWarnings("resource") | ||
|
|
@@ -30,12 +31,28 @@ public abstract class AbstractIntegrationTest { | |
| new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0")); | ||
|
|
||
| static { | ||
| POSTGRES.start(); | ||
| KAFKA.start(); | ||
| try { | ||
| POSTGRES.start(); | ||
| KAFKA.start(); | ||
| } catch (RuntimeException ex) { | ||
| safeStop(KAFKA); | ||
| safeStop(POSTGRES); | ||
| throw ex; | ||
| } | ||
| Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||
| KAFKA.stop(); | ||
| POSTGRES.stop(); | ||
| })); | ||
| safeStop(KAFKA); | ||
| safeStop(POSTGRES); | ||
| }, "testcontainers-shutdown")); | ||
| } | ||
|
|
||
| private static void safeStop(Startable container) { | ||
| try { | ||
| if (container != null) { | ||
| container.stop(); | ||
| } | ||
| } catch (Exception ignored) { | ||
| // best-effort cleanup | ||
| } | ||
| } | ||
|
Comment on lines
+48
to
56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider logging swallowed exceptions for debuggability. Silently swallowing exceptions is acceptable for best-effort cleanup, but a ♻️ Optional: add logging for suppressed exceptions+ private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(AbstractIntegrationTest.class);
+
private static void safeStop(Startable container) {
try {
if (container != null) {
container.stop();
}
- } catch (Exception ignored) {
- // best-effort cleanup
+ } catch (Exception ex) {
+ LOG.warn("Best-effort container stop failed", ex);
}
}🤖 Prompt for AI Agents |
||
|
|
||
| @Autowired | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| import org.springframework.test.context.DynamicPropertySource; | ||
| import org.testcontainers.containers.KafkaContainer; | ||
| import org.testcontainers.containers.PostgreSQLContainer; | ||
| import org.testcontainers.lifecycle.Startable; | ||
| import org.testcontainers.utility.DockerImageName; | ||
|
|
||
| @SuppressWarnings("resource") | ||
|
|
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest { | |
| new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0")); | ||
|
|
||
| static { | ||
| POSTGRES.start(); | ||
| KAFKA.start(); | ||
| try { | ||
| POSTGRES.start(); | ||
| KAFKA.start(); | ||
| } catch (RuntimeException ex) { | ||
| safeStop(KAFKA); | ||
| safeStop(POSTGRES); | ||
| throw ex; | ||
| } | ||
| Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||
| KAFKA.stop(); | ||
| POSTGRES.stop(); | ||
| })); | ||
| safeStop(KAFKA); | ||
| safeStop(POSTGRES); | ||
| }, "testcontainers-shutdown")); | ||
| } | ||
|
|
||
| private static void safeStop(Startable container) { | ||
| try { | ||
| if (container != null) { | ||
| container.stop(); | ||
| } | ||
| } catch (Exception ignored) { | ||
| // best-effort cleanup | ||
| } | ||
| } | ||
|
Comment on lines
+46
to
54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep teardown failures visible. Line 51 suppresses the stop exception completely. If Docker/Testcontainers cleanup fails, leftover containers become much harder to diagnose in CI because the failure never surfaces. Keep the best-effort semantics, but emit a warning with a fixed container label before continuing. The same helper was copied into the sibling 🤖 Prompt for AI Agents |
||
|
|
||
| @Autowired | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| import org.springframework.test.context.DynamicPropertySource; | ||
| import org.testcontainers.containers.KafkaContainer; | ||
| import org.testcontainers.containers.PostgreSQLContainer; | ||
| import org.testcontainers.lifecycle.Startable; | ||
| import org.testcontainers.utility.DockerImageName; | ||
|
|
||
| @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | ||
|
|
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest { | |
| new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0")); | ||
|
|
||
| static { | ||
| POSTGRES.start(); | ||
| KAFKA.start(); | ||
| try { | ||
| POSTGRES.start(); | ||
| KAFKA.start(); | ||
| } catch (RuntimeException ex) { | ||
| safeStop(KAFKA); | ||
| safeStop(POSTGRES); | ||
| throw ex; | ||
| } | ||
| Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||
| KAFKA.stop(); | ||
| POSTGRES.stop(); | ||
| })); | ||
| safeStop(KAFKA); | ||
| safeStop(POSTGRES); | ||
| }, "testcontainers-shutdown")); | ||
| } | ||
|
|
||
| private static void safeStop(Startable container) { | ||
| try { | ||
| if (container != null) { | ||
| container.stop(); | ||
| } | ||
| } catch (Exception ignored) { | ||
| // best-effort cleanup | ||
| } | ||
|
Comment on lines
+46
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Avoid fully silent stop failures in Suppressing exceptions without any trace makes flaky container teardown harder to triage. Keep best-effort semantics, but emit a low-level log with container type. Proposed diff+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
@@
public abstract class AbstractIntegrationTest {
+ private static final Logger log = LoggerFactory.getLogger(AbstractIntegrationTest.class);
@@
private static void safeStop(Startable container) {
try {
if (container != null) {
container.stop();
}
- } catch (Exception ignored) {
- // best-effort cleanup
+ } catch (Exception ex) {
+ log.debug("Best-effort Testcontainers stop failed for {}",
+ container == null ? "unknown" : container.getClass().getSimpleName(), ex);
}
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @DynamicPropertySource | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider minimal diagnostic signal for suppressed stop failures.
safeStopcurrently swallows all exceptions silently. Add a low-noise debug/warn log (without sensitive payloads) to aid flaky CI triage.🤖 Prompt for AI Agents