Thursday, December 21, 2023

Looking into Testcontainers . . .

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.

References

No comments:

Post a Comment