From dc4efaf2763c0698cbba1af6e038aa4e576c8fa1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 30 Apr 2023 16:48:33 -0700 Subject: [PATCH] Initialize containers first and destroy them last Update `TestcontainersLifecycleBeanPostProcessor` so that on initialization of the first bean all `Container` instances are started. With this update all `Container` beans will be started first in the `preInstantiateSingletons` phase and destroyed last. Closes gh-35223 --- ...tcontainersLifecycleBeanPostProcessor.java | 35 +++++- ...tainersLifecycleOrderIntegrationTests.java | 113 ++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleOrderIntegrationTests.java diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java index ef37027370..5316485d5e 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java @@ -16,6 +16,14 @@ package org.springframework.boot.testcontainers.lifecycle; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.testcontainers.containers.ContainerState; import org.testcontainers.containers.GenericContainer; import org.testcontainers.lifecycle.Startable; @@ -27,10 +35,16 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.log.LogMessage; /** * {@link BeanPostProcessor} to manage the lifecycle of {@link Startable startable * containers}. + *

+ * As well as starting containers, this {@link BeanPostProcessor} will also ensure that + * all containers are started as early as possible in the + * {@link ConfigurableListableBeanFactory#preInstantiateSingletons() pre-instantiate + * singletons} phase. * * @author Phillip Webb * @author Stephane Nicoll @@ -39,7 +53,11 @@ import org.springframework.core.annotation.Order; @Order(Ordered.LOWEST_PRECEDENCE) class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPostProcessor { - private final ConfigurableListableBeanFactory beanFactory; + private static final Log logger = LogFactory.getLog(TestcontainersLifecycleBeanPostProcessor.class); + + private ConfigurableListableBeanFactory beanFactory; + + private AtomicBoolean initializedContainers = new AtomicBoolean(); TestcontainersLifecycleBeanPostProcessor(ConfigurableListableBeanFactory beanFactory) { this.beanFactory = beanFactory; @@ -50,9 +68,24 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo if (bean instanceof Startable startable) { startable.start(); } + if (this.beanFactory.isConfigurationFrozen()) { + initializeContainers(); + } return bean; } + private void initializeContainers() { + if (this.initializedContainers.compareAndSet(false, true)) { + Set beanNames = new LinkedHashSet<>(); + beanNames.addAll(List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false))); + beanNames.addAll(List.of(this.beanFactory.getBeanNamesForType(Startable.class, false, false))); + for (String beanName : beanNames) { + logger.debug(LogMessage.format("Initializing container bean '%s'", beanName)); + this.beanFactory.getBean(beanName); + } + } + } + @Override public boolean requiresDestruction(Object bean) { return bean instanceof Startable; diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleOrderIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleOrderIntegrationTests.java new file mode 100644 index 0000000000..8b98eb7ce3 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleOrderIntegrationTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testcontainers.lifecycle; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; + +import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleOrderIntegrationTests.AssertingSpringExtension; +import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleOrderIntegrationTests.ContainerConfig; +import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleOrderIntegrationTests.TestConfig; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.RedisContainer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link TestcontainersLifecycleApplicationContextInitializer} to + * ensure create and destroy events happen in the correct order. + * + * @author Phillip Webb + */ +@ExtendWith(AssertingSpringExtension.class) +@ContextConfiguration(classes = { TestConfig.class, ContainerConfig.class }) +@DirtiesContext +public class TestcontainersLifecycleOrderIntegrationTests { + + static List events = Collections.synchronizedList(new ArrayList<>()); + + @Test + void eventsAreOrderedCorrectlyAfterStartup() { + assertThat(events).containsExactly("start-container", "create-bean"); + } + + @Configuration(proxyBeanMethods = false) + static class ContainerConfig { + + @Bean + @ServiceConnection("redis") + RedisContainer redisContainer() { + return new RedisContainer() { + + @Override + public void start() { + events.add("start-container"); + super.start(); + } + + @Override + public void stop() { + events.add("stop-container"); + super.stop(); + } + + }; + } + + } + + @Configuration(proxyBeanMethods = false) + static class TestConfig { + + @Bean + TestBean testBean() { + events.add("create-bean"); + return new TestBean(); + } + + } + + static class TestBean implements AutoCloseable { + + @Override + public void close() throws Exception { + events.add("destroy-bean"); + } + + } + + static class AssertingSpringExtension extends SpringExtension { + + @Override + public void afterAll(ExtensionContext context) throws Exception { + super.afterAll(context); + assertThat(events).containsExactly("start-container", "create-bean", "destroy-bean", "stop-container"); + } + + } + +}