Everything is easy and stays easy only up until experienced. In software development the "Aha-moment" of someone would be a "Huh-moment" of some other. We hear the phrase "It worked for me" almost daily that whispers aloud "It didn't work for someone else".
This blog post is not about Testcontainers but an issue that took awhile for me to find a workaround for. Testcontainers is a framework that provides lightweight throwaway instance for anything that run in a docker container. It's just appropriate for using in integration tests that require resources like Database server, Redis instance etc.
With the enhanced support in Spring Boot 3, Testcontainers got much simpler and better. Much of this enhanced support comes in as a new annotation: @ServieConnection. A bean method annotated with this annotation is enough and Spring Boot takes care of the rest - managing the life cycle of the container like staring at the application startup and stopping, establishing connection to the service running in the container etc. No other configuration like datasource connection is needed. Check this detailed Spring team blogpost on this.
The enthusiasm of "Lets give it a try", I got stuck for a couple of hours with a problem that I couldn't find a solution by tirelessly by trying many possible including googling, reading and what not.
Environment: Java 20, Spring Boot 3.2.1, maven 3.9.6 on macOS Catalina 10.15.7
The Scenario
The scenario is simple. I wanted to quickly try the newly enhanced and improved Spring Boot support for Testcontainers with PostgreSQL database.
However, I got stuck with getting the container started successfully for PostgreSQL database image. Started with the latest Postgres image tag, tried specific older tags including 16, 16.1. Downgraded testcontainer library versions from the latest 1.19.3 to 1.18.3 and few other as I hit for people who faced similar issue said it worked with other versions.
For everything that I tried, the issue was at least consistent with the following stacktrace:
2023-12-20T12:07:56.443-05:00 INFO 60313 --- [boot-graalvm] [ main] tc.postgres:16 : Container postgres:16 is starting: 995cb534ef6982062dcc98d9841cdf7962c0e1d37c809f3e479d06667cc567f7
2023-12-20T12:08:56.871-05:00 ERROR 60313 --- [boot-graalvm] [ main] tc.postgres:16 : Could not start container
java.lang.IllegalStateException: Failed to load ApplicationContext for [WebMergedContextConfiguration@2df9b4f3 testClass = com.giri.boot.graalvm.GraalVmApplicationIT, locations = [], classes = [com.giri.boot.graalvm.GraalVmApplication], contextInitializerClasses = [], activeProfiles = ["test"], propertySourceDescriptors = [], propertySourceProperties = ["org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true"], contextCustomizers = [[ImportsContextCustomizer@6112390a key = [com.giri.boot.graalvm.config.TestContainersConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@bdc8014, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@13047d7d, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@675ffd1d, org.springframework.boot.test.autoconfigure.actuate.observability.ObservabilityContextCustomizerFactory$DisableObservabilityContextCustomizer@1f, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizer@3104351d, org.spockframework.spring.mock.SpockContextCustomizer@0, org.springframework.boot.testcontainers.service.connection.ServiceConnectionContextCustomizer@0, org.springframework.boot.test.context.SpringBootTestAnnotation@156baa3c], resourceBasePath = "src/main/webapp", contextLoader = org.springframework.boot.test.context.SpringBootContextLoader, parent = null]
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:180)
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:130)
at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:191)
at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:130)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:247)
at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:163)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:310)
at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:735)
at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734)
at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
at java.base/java.util.Optional.orElseGet(Optional.java:364)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'graalVmApplication': Error creating bean with name 'postgreSQLContainer' defined in class path resource [com/giri/boot/graalvm/config/TestContainersConfiguration.class]: Container startup failed for image postgres:16
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:608)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:946)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:616)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:753)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:455)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:323)
at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$3(SpringBootContextLoader.java:137)
at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58)
at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46)
at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1442)
at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:552)
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:137)
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:108)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:225)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:152)
... 17 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'postgreSQLContainer' defined in class path resource [com/giri/boot/graalvm/config/TestContainersConfiguration.class]: Container startup failed for image postgres:16
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:608)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleBeanPostProcessor.getBeans(TestcontainersLifecycleBeanPostProcessor.java:128)
at org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleBeanPostProcessor.initializeContainers(TestcontainersLifecycleBeanPostProcessor.java:119)
at org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleBeanPostProcessor.postProcessAfterInitialization(TestcontainersLifecycleBeanPostProcessor.java:79)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:437)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1778)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:601)
... 37 more
Caused by: org.testcontainers.containers.ContainerLaunchException: Container startup failed for image postgres:16
at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:362)
at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:333)
at java.base/java.lang.Iterable.forEach(Iterable.java:75)
at org.springframework.boot.testcontainers.lifecycle.TestcontainersStartup$1.start(TestcontainersStartup.java:43)
at org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleBeanPostProcessor.start(TestcontainersLifecycleBeanPostProcessor.java:114)
at org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleBeanPostProcessor.initializeStartables(TestcontainersLifecycleBeanPostProcessor.java:103)
at org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleBeanPostProcessor.postProcessAfterInitialization(TestcontainersLifecycleBeanPostProcessor.java:83)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:437)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1778)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:601)
... 48 more
Caused by: org.rnorth.ducttape.RetryCountExceededException: Retry limit hit with exception
at org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess(Unreliables.java:88)
at org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:347)
... 57 more
Caused by: org.testcontainers.containers.ContainerLaunchException: Could not create/start container
at org.testcontainers.containers.GenericContainer.tryStart(GenericContainer.java:566)
at org.testcontainers.containers.GenericContainer.lambda$doStart$0(GenericContainer.java:357)
at org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess(Unreliables.java:81)
... 58 more
Caused by: java.lang.IllegalStateException: Wait strategy failed. Container exited with code 1
at org.testcontainers.containers.GenericContainer.tryStart(GenericContainer.java:536)
... 60 more
Caused by: org.testcontainers.containers.ContainerLaunchException: Timed out waiting for log output matching '.*database system is ready to accept connections.*\s'
at org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy.waitUntilReady(LogMessageWaitStrategy.java:47)
at org.testcontainers.containers.wait.strategy.AbstractWaitStrategy.waitUntilReady(AbstractWaitStrategy.java:52)
at org.testcontainers.containers.PostgreSQLContainer.waitUntilContainerStarted(PostgreSQLContainer.java:147)
at org.testcontainers.containers.GenericContainer.tryStart(GenericContainer.java:503)
... 60 more
Finally the logs from the container Docker dashboard for postgres:16 container that failed with the message was the last resort looked into:
popen failure: Cannot allocate memory
initdb: error: program "postgres" is needed by initdb but was not found in the same directory as "/usr/lib/postgresql/16/bin/initdb",
That wasn't that helpful. By googling for the above error message, I landed at a blog post. The solution that was described there worked for me as well.
The solution
Use Alpine Postgres. Diving further deeper is not worth my time at this time. I had to move on with what worked for me ;). Couldn't resist that saying, "it (also) worked for me".
The following is the code snippet:
@TestConfiguration(proxyBeanMethods = false)
@Slf4j
public class TestContainersConfiguration {
private static final String POSTGRES_IMAGE_TAG = "postgres:16-alpine"; // Issue: postgres:latest or postgres:16
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgreSQLContainer() {
log.info("PostgreSQLContainer bean...");
return new PostgreSQLContainer<>(DockerImageName.parse(POSTGRES_IMAGE_TAG))
.withDatabaseName("boot-graalvm")
.withUsername("postgres")
.withPassword("s3cr3t");
}
}
What is Alpine Linux?
Alpine Linux is a lightweight Linux distribution with the focus on SSS distribution - Small.Simple.Secure. It is famous for its small size with the fastest boot time, hence heavily used in containers.
💡 TIPS
Check your docker desktop version. If you have old version like what I have: 2.5.0.1, it is time to upgrade to newer version that works. For Mac OS Catalina, the newer version that works is 4.15.0.
Check this blog post on this.
Conclusion
The elevated development experience with underpinned layers of languages, frameworks, and systems increase the depth of things to know and understand for a Software Engineer, keeping the self-training capabilities and building knowledge, the real and not artificial, a daily challenge.
No comments:
Post a Comment