Skip to content

Commit 6ae113c

Browse files
committed
Fix parallel startup of testcontainers
Update `TestcontainersLifecycleBeanPostProcessor` so that containers can actually be started in parallel. Prior to this commit, `initializeStartables` would collect beans and in the process trigger the `postProcessAfterInitialization` method on each bean. This would see that `startablesInitialized` was `true` and call `startableBean.start` directly. The result of this was that beans were actually started sequentially and when the `start` method was finally called it had nothing to do. The updated code uses an enum rather than a boolean so that the `postProcessAfterInitialization` method no longer attempts to start beans unless `initializeStartables` has finished. Fixes gh-38831
1 parent 92a4a11 commit 6ae113c

File tree

2 files changed

+98
-5
lines changed

2 files changed

+98
-5
lines changed

spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Set;
2323
import java.util.concurrent.atomic.AtomicBoolean;
24+
import java.util.concurrent.atomic.AtomicReference;
2425
import java.util.stream.Collectors;
2526

2627
import org.apache.commons.logging.Log;
@@ -63,7 +64,7 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo
6364

6465
private final TestcontainersStartup startup;
6566

66-
private final AtomicBoolean startablesInitialized = new AtomicBoolean();
67+
private final AtomicReference<Startables> startables = new AtomicReference<>(Startables.UNSTARTED);
6768

6869
private final AtomicBoolean containersInitialized = new AtomicBoolean();
6970

@@ -79,28 +80,33 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw
7980
initializeContainers();
8081
}
8182
if (bean instanceof Startable startableBean) {
82-
if (this.startablesInitialized.compareAndSet(false, true)) {
83+
if (this.startables.compareAndExchange(Startables.UNSTARTED, Startables.STARTING) == Startables.UNSTARTED) {
8384
initializeStartables(startableBean, beanName);
8485
}
85-
else {
86+
else if (this.startables.get() == Startables.STARTED) {
87+
logger.trace(LogMessage.format("Starting container %s", beanName));
8688
startableBean.start();
8789
}
8890
}
8991
return bean;
9092
}
9193

9294
private void initializeStartables(Startable startableBean, String startableBeanName) {
95+
logger.trace(LogMessage.format("Initializing startables"));
9396
List<String> beanNames = new ArrayList<>(
9497
List.of(this.beanFactory.getBeanNamesForType(Startable.class, false, false)));
9598
beanNames.remove(startableBeanName);
9699
List<Object> beans = getBeans(beanNames);
97100
if (beans == null) {
98-
this.startablesInitialized.set(false);
101+
logger.trace(LogMessage.format("Failed to obtain startables %s", beanNames));
102+
this.startables.set(Startables.UNSTARTED);
99103
return;
100104
}
101105
beanNames.add(startableBeanName);
102106
beans.add(startableBean);
107+
logger.trace(LogMessage.format("Starting startables %s", beanNames));
103108
start(beans);
109+
this.startables.set(Startables.STARTED);
104110
if (!beanNames.isEmpty()) {
105111
logger.debug(LogMessage.format("Initialized and started startable beans '%s'", beanNames));
106112
}
@@ -115,8 +121,14 @@ private void start(List<Object> beans) {
115121
}
116122

117123
private void initializeContainers() {
124+
logger.trace("Initializing containers");
118125
List<String> beanNames = List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false));
119-
if (getBeans(beanNames) == null) {
126+
List<Object> beans = getBeans(beanNames);
127+
if (beans != null) {
128+
logger.trace(LogMessage.format("Initialized containers %s", beanNames));
129+
}
130+
else {
131+
logger.trace(LogMessage.format("Failed to initialize containers %s", beanNames));
120132
this.containersInitialized.set(false);
121133
}
122134
}
@@ -164,4 +176,10 @@ private boolean isReusedContainer(Object bean) {
164176
return (bean instanceof GenericContainer<?> container) && container.isShouldBeReused();
165177
}
166178

179+
enum Startables {
180+
181+
UNSTARTED, STARTING, STARTED
182+
183+
}
184+
167185
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.testcontainers.lifecycle;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.ExtendWith;
21+
import org.testcontainers.containers.PostgreSQLContainer;
22+
23+
import org.springframework.boot.test.system.CapturedOutput;
24+
import org.springframework.boot.test.system.OutputCaptureExtension;
25+
import org.springframework.boot.testcontainers.lifecycle.TestContainersParallelStartupIntegrationTests.ContainerConfig;
26+
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
27+
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.test.annotation.DirtiesContext;
31+
import org.springframework.test.context.ContextConfiguration;
32+
import org.springframework.test.context.TestPropertySource;
33+
import org.springframework.test.context.junit.jupiter.SpringExtension;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
37+
/**
38+
* Integration test for parallel startup.
39+
*
40+
* @author Phillip Webb
41+
*/
42+
@ExtendWith(SpringExtension.class)
43+
@ContextConfiguration(classes = ContainerConfig.class)
44+
@TestPropertySource(properties = "spring.testcontainers.beans.startup=parallel")
45+
@DirtiesContext
46+
@DisabledIfDockerUnavailable
47+
@ExtendWith(OutputCaptureExtension.class)
48+
public class TestContainersParallelStartupIntegrationTests {
49+
50+
@Test
51+
void startsInParallel(CapturedOutput out) {
52+
assertThat(out).contains("-lifecycle-0").contains("-lifecycle-1").contains("-lifecycle-2");
53+
}
54+
55+
@Configuration(proxyBeanMethods = false)
56+
static class ContainerConfig {
57+
58+
@Bean
59+
static PostgreSQLContainer<?> container1() {
60+
return new PostgreSQLContainer<>(DockerImageNames.postgresql());
61+
}
62+
63+
@Bean
64+
static PostgreSQLContainer<?> container2() {
65+
return new PostgreSQLContainer<>(DockerImageNames.postgresql());
66+
}
67+
68+
@Bean
69+
static PostgreSQLContainer<?> container3() {
70+
return new PostgreSQLContainer<>(DockerImageNames.postgresql());
71+
}
72+
73+
}
74+
75+
}

0 commit comments

Comments
 (0)