From 1aca676e573bdf48822fc6c76504247a6df9edc6 Mon Sep 17 00:00:00 2001 From: Puneethkumar CK Date: Tue, 17 Mar 2026 06:42:48 +0100 Subject: [PATCH] fix(infra): harden Testcontainers shutdown hooks with safeStop (STA-189) Wrap container startup in try/catch so that if a later container fails to start, already-started containers are stopped instead of orphaned. Replace bare stop() calls in shutdown hooks with a safeStop() helper that catches exceptions, ensuring one container failure does not prevent the remaining containers from being cleaned up. Name the shutdown thread "testcontainers-shutdown" for easier diagnostics. Applied consistently across all 10 AbstractIntegrationTest files. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../gateway/iam/AbstractIntegrationTest.java | 32 ++++++++++++---- .../custody/AbstractIntegrationTest.java | 27 +++++++++++--- .../compliance/AbstractIntegrationTest.java | 27 +++++++++++--- .../offramp/AbstractIntegrationTest.java | 27 +++++++++++--- .../onramp/AbstractIntegrationTest.java | 27 +++++++++++--- .../payments/fx/AbstractIntegrationTest.java | 27 +++++++++++--- .../ledger/AbstractIntegrationTest.java | 27 +++++++++++--- .../merchant/iam/AbstractIntegrationTest.java | 37 ++++++++++++++----- .../onboarding/AbstractIntegrationTest.java | 27 +++++++++++--- .../orchestrator/AbstractIntegrationTest.java | 27 +++++++++++--- 10 files changed, 229 insertions(+), 56 deletions(-) diff --git a/api-gateway-iam/api-gateway-iam/src/integration-test/java/com/stablecoin/payments/gateway/iam/AbstractIntegrationTest.java b/api-gateway-iam/api-gateway-iam/src/integration-test/java/com/stablecoin/payments/gateway/iam/AbstractIntegrationTest.java index 713a004e..fd1c0c20 100644 --- a/api-gateway-iam/api-gateway-iam/src/integration-test/java/com/stablecoin/payments/gateway/iam/AbstractIntegrationTest.java +++ b/api-gateway-iam/api-gateway-iam/src/integration-test/java/com/stablecoin/payments/gateway/iam/AbstractIntegrationTest.java @@ -11,6 +11,7 @@ import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.lifecycle.Startable; import org.testcontainers.utility.DockerImageName; @SuppressWarnings("resource") @@ -33,14 +34,31 @@ public abstract class AbstractIntegrationTest { .withExposedPorts(6379); static { - POSTGRES.start(); - KAFKA.start(); - REDIS.start(); + try { + POSTGRES.start(); + KAFKA.start(); + REDIS.start(); + } catch (RuntimeException ex) { + safeStop(REDIS); + safeStop(KAFKA); + safeStop(POSTGRES); + throw ex; + } Runtime.getRuntime().addShutdownHook(new Thread(() -> { - REDIS.stop(); - KAFKA.stop(); - POSTGRES.stop(); - })); + safeStop(REDIS); + safeStop(KAFKA); + safeStop(POSTGRES); + }, "testcontainers-shutdown")); + } + + private static void safeStop(Startable container) { + try { + if (container != null) { + container.stop(); + } + } catch (Exception ignored) { + // best-effort cleanup + } } @Autowired diff --git a/blockchain-custody/blockchain-custody/src/integration-test/java/com/stablecoin/payments/custody/AbstractIntegrationTest.java b/blockchain-custody/blockchain-custody/src/integration-test/java/com/stablecoin/payments/custody/AbstractIntegrationTest.java index 8b0f203e..c87e715b 100644 --- a/blockchain-custody/blockchain-custody/src/integration-test/java/com/stablecoin/payments/custody/AbstractIntegrationTest.java +++ b/blockchain-custody/blockchain-custody/src/integration-test/java/com/stablecoin/payments/custody/AbstractIntegrationTest.java @@ -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 + } } @Autowired diff --git a/compliance-travel-rule/compliance-travel-rule/src/integration-test/java/com/stablecoin/payments/compliance/AbstractIntegrationTest.java b/compliance-travel-rule/compliance-travel-rule/src/integration-test/java/com/stablecoin/payments/compliance/AbstractIntegrationTest.java index 9bc4b2c9..6a83ac5b 100644 --- a/compliance-travel-rule/compliance-travel-rule/src/integration-test/java/com/stablecoin/payments/compliance/AbstractIntegrationTest.java +++ b/compliance-travel-rule/compliance-travel-rule/src/integration-test/java/com/stablecoin/payments/compliance/AbstractIntegrationTest.java @@ -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 + } } @Autowired diff --git a/fiat-off-ramp/fiat-off-ramp/src/integration-test/java/com/stablecoin/payments/offramp/AbstractIntegrationTest.java b/fiat-off-ramp/fiat-off-ramp/src/integration-test/java/com/stablecoin/payments/offramp/AbstractIntegrationTest.java index 1de0c74f..e6ea27ff 100644 --- a/fiat-off-ramp/fiat-off-ramp/src/integration-test/java/com/stablecoin/payments/offramp/AbstractIntegrationTest.java +++ b/fiat-off-ramp/fiat-off-ramp/src/integration-test/java/com/stablecoin/payments/offramp/AbstractIntegrationTest.java @@ -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 + } } @Autowired diff --git a/fiat-on-ramp/fiat-on-ramp/src/integration-test/java/com/stablecoin/payments/onramp/AbstractIntegrationTest.java b/fiat-on-ramp/fiat-on-ramp/src/integration-test/java/com/stablecoin/payments/onramp/AbstractIntegrationTest.java index 6a9d7ddd..42dd772e 100644 --- a/fiat-on-ramp/fiat-on-ramp/src/integration-test/java/com/stablecoin/payments/onramp/AbstractIntegrationTest.java +++ b/fiat-on-ramp/fiat-on-ramp/src/integration-test/java/com/stablecoin/payments/onramp/AbstractIntegrationTest.java @@ -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 + } } @Autowired diff --git a/fx-liquidity-engine/fx-liquidity-engine/src/integration-test/java/com/stablecoin/payments/fx/AbstractIntegrationTest.java b/fx-liquidity-engine/fx-liquidity-engine/src/integration-test/java/com/stablecoin/payments/fx/AbstractIntegrationTest.java index a0cd5ae2..2e4347e4 100644 --- a/fx-liquidity-engine/fx-liquidity-engine/src/integration-test/java/com/stablecoin/payments/fx/AbstractIntegrationTest.java +++ b/fx-liquidity-engine/fx-liquidity-engine/src/integration-test/java/com/stablecoin/payments/fx/AbstractIntegrationTest.java @@ -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 + } } @Autowired diff --git a/ledger-accounting/ledger-accounting/src/integration-test/java/com/stablecoin/payments/ledger/AbstractIntegrationTest.java b/ledger-accounting/ledger-accounting/src/integration-test/java/com/stablecoin/payments/ledger/AbstractIntegrationTest.java index e6d32b53..8613a8de 100644 --- a/ledger-accounting/ledger-accounting/src/integration-test/java/com/stablecoin/payments/ledger/AbstractIntegrationTest.java +++ b/ledger-accounting/ledger-accounting/src/integration-test/java/com/stablecoin/payments/ledger/AbstractIntegrationTest.java @@ -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 + } } @Autowired diff --git a/merchant-iam/merchant-iam/src/integration-test/java/com/stablecoin/payments/merchant/iam/AbstractIntegrationTest.java b/merchant-iam/merchant-iam/src/integration-test/java/com/stablecoin/payments/merchant/iam/AbstractIntegrationTest.java index eb3453ce..d3a1e80a 100644 --- a/merchant-iam/merchant-iam/src/integration-test/java/com/stablecoin/payments/merchant/iam/AbstractIntegrationTest.java +++ b/merchant-iam/merchant-iam/src/integration-test/java/com/stablecoin/payments/merchant/iam/AbstractIntegrationTest.java @@ -14,6 +14,7 @@ import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.lifecycle.Startable; import org.testcontainers.utility.DockerImageName; import java.util.UUID; @@ -46,16 +47,34 @@ public abstract class AbstractIntegrationTest { .withExposedPorts(6379); static { - POSTGRES.start(); - KAFKA.start(); - MAILPIT.start(); - REDIS.start(); + try { + POSTGRES.start(); + KAFKA.start(); + MAILPIT.start(); + REDIS.start(); + } catch (RuntimeException ex) { + safeStop(REDIS); + safeStop(MAILPIT); + safeStop(KAFKA); + safeStop(POSTGRES); + throw ex; + } Runtime.getRuntime().addShutdownHook(new Thread(() -> { - REDIS.stop(); - MAILPIT.stop(); - KAFKA.stop(); - POSTGRES.stop(); - })); + safeStop(REDIS); + safeStop(MAILPIT); + safeStop(KAFKA); + safeStop(POSTGRES); + }, "testcontainers-shutdown")); + } + + private static void safeStop(Startable container) { + try { + if (container != null) { + container.stop(); + } + } catch (Exception ignored) { + // best-effort cleanup + } } @Autowired diff --git a/merchant-onboarding/merchant-onboarding/src/integration-test/java/com/stablecoin/payments/merchant/onboarding/AbstractIntegrationTest.java b/merchant-onboarding/merchant-onboarding/src/integration-test/java/com/stablecoin/payments/merchant/onboarding/AbstractIntegrationTest.java index 3fbc7a62..c66f6e03 100644 --- a/merchant-onboarding/merchant-onboarding/src/integration-test/java/com/stablecoin/payments/merchant/onboarding/AbstractIntegrationTest.java +++ b/merchant-onboarding/merchant-onboarding/src/integration-test/java/com/stablecoin/payments/merchant/onboarding/AbstractIntegrationTest.java @@ -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 + } } @DynamicPropertySource diff --git a/payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/AbstractIntegrationTest.java b/payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/AbstractIntegrationTest.java index 75cbfbbd..09aed137 100644 --- a/payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/AbstractIntegrationTest.java +++ b/payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/AbstractIntegrationTest.java @@ -11,6 +11,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") @@ -29,12 +30,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 + } } @Autowired