Skip to content

Commit 1d5a0c7

Browse files
fix(infra): harden Testcontainers shutdown hooks with safeStop (STA-189) (#190)
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) <noreply@anthropic.com>
1 parent 1b797b3 commit 1d5a0c7

10 files changed

Lines changed: 229 additions & 56 deletions

File tree

api-gateway-iam/api-gateway-iam/src/integration-test/java/com/stablecoin/payments/gateway/iam/AbstractIntegrationTest.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.testcontainers.containers.GenericContainer;
1212
import org.testcontainers.containers.KafkaContainer;
1313
import org.testcontainers.containers.PostgreSQLContainer;
14+
import org.testcontainers.lifecycle.Startable;
1415
import org.testcontainers.utility.DockerImageName;
1516

1617
@SuppressWarnings("resource")
@@ -33,14 +34,31 @@ public abstract class AbstractIntegrationTest {
3334
.withExposedPorts(6379);
3435

3536
static {
36-
POSTGRES.start();
37-
KAFKA.start();
38-
REDIS.start();
37+
try {
38+
POSTGRES.start();
39+
KAFKA.start();
40+
REDIS.start();
41+
} catch (RuntimeException ex) {
42+
safeStop(REDIS);
43+
safeStop(KAFKA);
44+
safeStop(POSTGRES);
45+
throw ex;
46+
}
3947
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
40-
REDIS.stop();
41-
KAFKA.stop();
42-
POSTGRES.stop();
43-
}));
48+
safeStop(REDIS);
49+
safeStop(KAFKA);
50+
safeStop(POSTGRES);
51+
}, "testcontainers-shutdown"));
52+
}
53+
54+
private static void safeStop(Startable container) {
55+
try {
56+
if (container != null) {
57+
container.stop();
58+
}
59+
} catch (Exception ignored) {
60+
// best-effort cleanup
61+
}
4462
}
4563

4664
@Autowired

blockchain-custody/blockchain-custody/src/integration-test/java/com/stablecoin/payments/custody/AbstractIntegrationTest.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.test.context.DynamicPropertySource;
1111
import org.testcontainers.containers.KafkaContainer;
1212
import org.testcontainers.containers.PostgreSQLContainer;
13+
import org.testcontainers.lifecycle.Startable;
1314
import org.testcontainers.utility.DockerImageName;
1415

1516
@SuppressWarnings("resource")
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest {
2829
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0"));
2930

3031
static {
31-
POSTGRES.start();
32-
KAFKA.start();
32+
try {
33+
POSTGRES.start();
34+
KAFKA.start();
35+
} catch (RuntimeException ex) {
36+
safeStop(KAFKA);
37+
safeStop(POSTGRES);
38+
throw ex;
39+
}
3340
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
34-
KAFKA.stop();
35-
POSTGRES.stop();
36-
}));
41+
safeStop(KAFKA);
42+
safeStop(POSTGRES);
43+
}, "testcontainers-shutdown"));
44+
}
45+
46+
private static void safeStop(Startable container) {
47+
try {
48+
if (container != null) {
49+
container.stop();
50+
}
51+
} catch (Exception ignored) {
52+
// best-effort cleanup
53+
}
3754
}
3855

3956
@Autowired

compliance-travel-rule/compliance-travel-rule/src/integration-test/java/com/stablecoin/payments/compliance/AbstractIntegrationTest.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.test.context.DynamicPropertySource;
1111
import org.testcontainers.containers.KafkaContainer;
1212
import org.testcontainers.containers.PostgreSQLContainer;
13+
import org.testcontainers.lifecycle.Startable;
1314
import org.testcontainers.utility.DockerImageName;
1415

1516
@SuppressWarnings("resource")
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest {
2829
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0"));
2930

3031
static {
31-
POSTGRES.start();
32-
KAFKA.start();
32+
try {
33+
POSTGRES.start();
34+
KAFKA.start();
35+
} catch (RuntimeException ex) {
36+
safeStop(KAFKA);
37+
safeStop(POSTGRES);
38+
throw ex;
39+
}
3340
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
34-
KAFKA.stop();
35-
POSTGRES.stop();
36-
}));
41+
safeStop(KAFKA);
42+
safeStop(POSTGRES);
43+
}, "testcontainers-shutdown"));
44+
}
45+
46+
private static void safeStop(Startable container) {
47+
try {
48+
if (container != null) {
49+
container.stop();
50+
}
51+
} catch (Exception ignored) {
52+
// best-effort cleanup
53+
}
3754
}
3855

3956
@Autowired

fiat-off-ramp/fiat-off-ramp/src/integration-test/java/com/stablecoin/payments/offramp/AbstractIntegrationTest.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.test.context.DynamicPropertySource;
1111
import org.testcontainers.containers.KafkaContainer;
1212
import org.testcontainers.containers.PostgreSQLContainer;
13+
import org.testcontainers.lifecycle.Startable;
1314
import org.testcontainers.utility.DockerImageName;
1415

1516
@SuppressWarnings("resource")
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest {
2829
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0"));
2930

3031
static {
31-
POSTGRES.start();
32-
KAFKA.start();
32+
try {
33+
POSTGRES.start();
34+
KAFKA.start();
35+
} catch (RuntimeException ex) {
36+
safeStop(KAFKA);
37+
safeStop(POSTGRES);
38+
throw ex;
39+
}
3340
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
34-
KAFKA.stop();
35-
POSTGRES.stop();
36-
}));
41+
safeStop(KAFKA);
42+
safeStop(POSTGRES);
43+
}, "testcontainers-shutdown"));
44+
}
45+
46+
private static void safeStop(Startable container) {
47+
try {
48+
if (container != null) {
49+
container.stop();
50+
}
51+
} catch (Exception ignored) {
52+
// best-effort cleanup
53+
}
3754
}
3855

3956
@Autowired

fiat-on-ramp/fiat-on-ramp/src/integration-test/java/com/stablecoin/payments/onramp/AbstractIntegrationTest.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.test.context.DynamicPropertySource;
1111
import org.testcontainers.containers.KafkaContainer;
1212
import org.testcontainers.containers.PostgreSQLContainer;
13+
import org.testcontainers.lifecycle.Startable;
1314
import org.testcontainers.utility.DockerImageName;
1415

1516
@SuppressWarnings("resource")
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest {
2829
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0"));
2930

3031
static {
31-
POSTGRES.start();
32-
KAFKA.start();
32+
try {
33+
POSTGRES.start();
34+
KAFKA.start();
35+
} catch (RuntimeException ex) {
36+
safeStop(KAFKA);
37+
safeStop(POSTGRES);
38+
throw ex;
39+
}
3340
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
34-
KAFKA.stop();
35-
POSTGRES.stop();
36-
}));
41+
safeStop(KAFKA);
42+
safeStop(POSTGRES);
43+
}, "testcontainers-shutdown"));
44+
}
45+
46+
private static void safeStop(Startable container) {
47+
try {
48+
if (container != null) {
49+
container.stop();
50+
}
51+
} catch (Exception ignored) {
52+
// best-effort cleanup
53+
}
3754
}
3855

3956
@Autowired

fx-liquidity-engine/fx-liquidity-engine/src/integration-test/java/com/stablecoin/payments/fx/AbstractIntegrationTest.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.test.context.DynamicPropertySource;
1111
import org.testcontainers.containers.KafkaContainer;
1212
import org.testcontainers.containers.PostgreSQLContainer;
13+
import org.testcontainers.lifecycle.Startable;
1314
import org.testcontainers.utility.DockerImageName;
1415

1516
@SuppressWarnings("resource")
@@ -30,12 +31,28 @@ public abstract class AbstractIntegrationTest {
3031
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0"));
3132

3233
static {
33-
POSTGRES.start();
34-
KAFKA.start();
34+
try {
35+
POSTGRES.start();
36+
KAFKA.start();
37+
} catch (RuntimeException ex) {
38+
safeStop(KAFKA);
39+
safeStop(POSTGRES);
40+
throw ex;
41+
}
3542
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
36-
KAFKA.stop();
37-
POSTGRES.stop();
38-
}));
43+
safeStop(KAFKA);
44+
safeStop(POSTGRES);
45+
}, "testcontainers-shutdown"));
46+
}
47+
48+
private static void safeStop(Startable container) {
49+
try {
50+
if (container != null) {
51+
container.stop();
52+
}
53+
} catch (Exception ignored) {
54+
// best-effort cleanup
55+
}
3956
}
4057

4158
@Autowired

ledger-accounting/ledger-accounting/src/integration-test/java/com/stablecoin/payments/ledger/AbstractIntegrationTest.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.test.context.DynamicPropertySource;
1111
import org.testcontainers.containers.KafkaContainer;
1212
import org.testcontainers.containers.PostgreSQLContainer;
13+
import org.testcontainers.lifecycle.Startable;
1314
import org.testcontainers.utility.DockerImageName;
1415

1516
@SuppressWarnings("resource")
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest {
2829
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0"));
2930

3031
static {
31-
POSTGRES.start();
32-
KAFKA.start();
32+
try {
33+
POSTGRES.start();
34+
KAFKA.start();
35+
} catch (RuntimeException ex) {
36+
safeStop(KAFKA);
37+
safeStop(POSTGRES);
38+
throw ex;
39+
}
3340
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
34-
KAFKA.stop();
35-
POSTGRES.stop();
36-
}));
41+
safeStop(KAFKA);
42+
safeStop(POSTGRES);
43+
}, "testcontainers-shutdown"));
44+
}
45+
46+
private static void safeStop(Startable container) {
47+
try {
48+
if (container != null) {
49+
container.stop();
50+
}
51+
} catch (Exception ignored) {
52+
// best-effort cleanup
53+
}
3754
}
3855

3956
@Autowired

merchant-iam/merchant-iam/src/integration-test/java/com/stablecoin/payments/merchant/iam/AbstractIntegrationTest.java

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.testcontainers.containers.GenericContainer;
1515
import org.testcontainers.containers.KafkaContainer;
1616
import org.testcontainers.containers.PostgreSQLContainer;
17+
import org.testcontainers.lifecycle.Startable;
1718
import org.testcontainers.utility.DockerImageName;
1819

1920
import java.util.UUID;
@@ -46,16 +47,34 @@ public abstract class AbstractIntegrationTest {
4647
.withExposedPorts(6379);
4748

4849
static {
49-
POSTGRES.start();
50-
KAFKA.start();
51-
MAILPIT.start();
52-
REDIS.start();
50+
try {
51+
POSTGRES.start();
52+
KAFKA.start();
53+
MAILPIT.start();
54+
REDIS.start();
55+
} catch (RuntimeException ex) {
56+
safeStop(REDIS);
57+
safeStop(MAILPIT);
58+
safeStop(KAFKA);
59+
safeStop(POSTGRES);
60+
throw ex;
61+
}
5362
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
54-
REDIS.stop();
55-
MAILPIT.stop();
56-
KAFKA.stop();
57-
POSTGRES.stop();
58-
}));
63+
safeStop(REDIS);
64+
safeStop(MAILPIT);
65+
safeStop(KAFKA);
66+
safeStop(POSTGRES);
67+
}, "testcontainers-shutdown"));
68+
}
69+
70+
private static void safeStop(Startable container) {
71+
try {
72+
if (container != null) {
73+
container.stop();
74+
}
75+
} catch (Exception ignored) {
76+
// best-effort cleanup
77+
}
5978
}
6079

6180
@Autowired

merchant-onboarding/merchant-onboarding/src/integration-test/java/com/stablecoin/payments/merchant/onboarding/AbstractIntegrationTest.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.test.context.DynamicPropertySource;
1111
import org.testcontainers.containers.KafkaContainer;
1212
import org.testcontainers.containers.PostgreSQLContainer;
13+
import org.testcontainers.lifecycle.Startable;
1314
import org.testcontainers.utility.DockerImageName;
1415

1516
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@@ -28,12 +29,28 @@ public abstract class AbstractIntegrationTest {
2829
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0"));
2930

3031
static {
31-
POSTGRES.start();
32-
KAFKA.start();
32+
try {
33+
POSTGRES.start();
34+
KAFKA.start();
35+
} catch (RuntimeException ex) {
36+
safeStop(KAFKA);
37+
safeStop(POSTGRES);
38+
throw ex;
39+
}
3340
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
34-
KAFKA.stop();
35-
POSTGRES.stop();
36-
}));
41+
safeStop(KAFKA);
42+
safeStop(POSTGRES);
43+
}, "testcontainers-shutdown"));
44+
}
45+
46+
private static void safeStop(Startable container) {
47+
try {
48+
if (container != null) {
49+
container.stop();
50+
}
51+
} catch (Exception ignored) {
52+
// best-effort cleanup
53+
}
3754
}
3855

3956
@DynamicPropertySource

0 commit comments

Comments
 (0)