From 2684f12bed11e1fe0b6cf52f8195c143512c613c Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Mon, 21 Oct 2024 17:17:14 -0300 Subject: [PATCH] Enable AppCDS creation during Docker image build AppCDS (Application Class Data Sharing) is a JVM feature that allows preloading and sharing class metadata across JVM instances. This can significantly improve startup time and reduce memory usage, especially for large applications with many dependencies. This patch launches the applications during the Docker image build process for a short period of time (until the spring context is first refreshed) to build the AppCDS archive, and introduces an `AutoConfiguration` that: - Allows passing a JVM argument (`-Dspring.context.exit=`) to terminate the application upon specific Spring `ApplicationContextEvent` events. - Facilitates starting the application during Docker image builds to create the AppCDS archive, improving startup performance. Supported events include: - `onPrepared` - `onRefreshed` - `onStarted` - `onReady` Note: Spring Boot 3.2+ natively supports `spring.context.exit=onRefresh` as of [this commit](https://github.com/spring-projects/spring-framework/commit/eb3982b6c25d6c3dd49f6c4cc000c40364916a83), so this feature may not be necessary post-upgrade from Spring Boot 2.7. Additional events are included for cases where certain applications may require different startup stages for proper initialization. The new `offline` embedded Spring profile should also facilitate starting without Spring Cloud Bus, ACL, etc. --- compose/compose.yml | 2 +- src/apps/base-images/jre/Dockerfile | 15 ++- src/apps/base-images/spring-boot/Dockerfile | 14 -- src/apps/geoserver/gwc/Dockerfile | 12 ++ src/apps/geoserver/restconfig/Dockerfile | 12 ++ src/apps/geoserver/wcs/Dockerfile | 12 ++ src/apps/geoserver/webui/Dockerfile | 12 ++ src/apps/geoserver/wfs/Dockerfile | 12 ++ src/apps/geoserver/wms/Dockerfile | 12 ++ src/apps/geoserver/wps/Dockerfile | 12 ++ src/apps/infrastructure/admin/Dockerfile | 9 ++ src/apps/infrastructure/config/Dockerfile | 6 +- src/apps/infrastructure/discovery/Dockerfile | 9 ++ src/apps/infrastructure/gateway/Dockerfile | 9 ++ ...itOnApplicationEventAutoConfiguration.java | 122 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 3 +- .../resources/gs_cloud_bootstrap_profiles.yml | 11 ++ 17 files changed, 266 insertions(+), 18 deletions(-) create mode 100644 src/starters/spring-boot/src/main/java/org/geoserver/cloud/app/ExitOnApplicationEventAutoConfiguration.java diff --git a/compose/compose.yml b/compose/compose.yml index a45f9677d..06f7d5fd4 100644 --- a/compose/compose.yml +++ b/compose/compose.yml @@ -67,7 +67,7 @@ services: environment: JAVA_OPTS: "${JAVA_OPTS_CONFIG}" SPRING_PROFILES_ACTIVE: "${CONFIG_SERVER_DEFAULT_PROFILES}" - restart: unless-stopped + # restart: unless-stopped volumes: # override with the local copy to test config changes during development - config:/etc/geoserver diff --git a/src/apps/base-images/jre/Dockerfile b/src/apps/base-images/jre/Dockerfile index f9b747bcb..addc775d4 100644 --- a/src/apps/base-images/jre/Dockerfile +++ b/src/apps/base-images/jre/Dockerfile @@ -2,7 +2,20 @@ FROM eclipse-temurin:21-jre LABEL maintainer="GeoServer PSC " -ENV JAVA_TOOL_OPTIONS= +# default JVM parameters https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html +# to add JVM parameters use the JAVA_OPTS env variable preferrably +ENV DEFAULT_JAVA_TOOL_OPTIONS="\ +--add-opens=java.base/java.lang=ALL-UNNAMED \ +--add-opens=java.base/java.util=ALL-UNNAMED \ +--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \ +--add-opens=java.base/java.text=ALL-UNNAMED \ +--add-opens=java.desktop/java.awt.font=ALL-UNNAMED \ +--add-opens=java.desktop/sun.awt.image=ALL-UNNAMED \ +--add-opens=java.desktop/sun.java2d.pipe=ALL-UNNAMED \ +--add-opens=java.naming/com.sun.jndi.ldap=ALL-UNNAMED \ +-Djava.awt.headless=true" + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS}" ENV JAVA_OPTS= # Install the system CA certificates for the JVM :wqnow that we're root diff --git a/src/apps/base-images/spring-boot/Dockerfile b/src/apps/base-images/spring-boot/Dockerfile index bbffd5b53..83b5a20bf 100644 --- a/src/apps/base-images/spring-boot/Dockerfile +++ b/src/apps/base-images/spring-boot/Dockerfile @@ -18,20 +18,6 @@ RUN mkdir -p /opt/app/bin WORKDIR /opt/app/bin -# default JVM parameters https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html -# to add JVM parameters use the JAVA_OPTS env variable preferrably -ENV JAVA_TOOL_OPTIONS="\ ---add-opens=java.base/java.lang=ALL-UNNAMED \ ---add-opens=java.base/java.util=ALL-UNNAMED \ ---add-opens=java.base/java.lang.reflect=ALL-UNNAMED \ ---add-opens=java.base/java.text=ALL-UNNAMED \ ---add-opens=java.desktop/java.awt.font=ALL-UNNAMED \ ---add-opens=java.desktop/sun.awt.image=ALL-UNNAMED \ ---add-opens=java.desktop/sun.java2d.pipe=ALL-UNNAMED \ ---add-opens=java.naming/com.sun.jndi.ldap=ALL-UNNAMED \ --Djava.awt.headless=true" - -ENV JAVA_OPTS= EXPOSE 8080 EXPOSE 8081 diff --git a/src/apps/geoserver/gwc/Dockerfile b/src/apps/geoserver/gwc/Dockerfile index 35f9293be..4cb66258d 100644 --- a/src/apps/geoserver/gwc/Dockerfile +++ b/src/apps/geoserver/gwc/Dockerfile @@ -20,3 +20,15 @@ COPY --from=builder spring-boot-loader/ ./ #see https://github.com/moby/moby/issues/37965 RUN true COPY --from=builder application/ ./ + +# Execute the CDS training run +RUN mkdir /tmp/tmpdatadir +RUN java \ +-XX:ArchiveClassesAtExit=application.jsa \ +-Dspring.context.exit=onRefreshed \ +-Dspring.profiles.active=standalone,datadir,offline \ +-Dgeosever.backend.data-directory.location=/tmp/tmpdatadir \ +org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/apps/geoserver/restconfig/Dockerfile b/src/apps/geoserver/restconfig/Dockerfile index 35f9293be..4cb66258d 100644 --- a/src/apps/geoserver/restconfig/Dockerfile +++ b/src/apps/geoserver/restconfig/Dockerfile @@ -20,3 +20,15 @@ COPY --from=builder spring-boot-loader/ ./ #see https://github.com/moby/moby/issues/37965 RUN true COPY --from=builder application/ ./ + +# Execute the CDS training run +RUN mkdir /tmp/tmpdatadir +RUN java \ +-XX:ArchiveClassesAtExit=application.jsa \ +-Dspring.context.exit=onRefreshed \ +-Dspring.profiles.active=standalone,datadir,offline \ +-Dgeosever.backend.data-directory.location=/tmp/tmpdatadir \ +org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/apps/geoserver/wcs/Dockerfile b/src/apps/geoserver/wcs/Dockerfile index 35f9293be..4cb66258d 100644 --- a/src/apps/geoserver/wcs/Dockerfile +++ b/src/apps/geoserver/wcs/Dockerfile @@ -20,3 +20,15 @@ COPY --from=builder spring-boot-loader/ ./ #see https://github.com/moby/moby/issues/37965 RUN true COPY --from=builder application/ ./ + +# Execute the CDS training run +RUN mkdir /tmp/tmpdatadir +RUN java \ +-XX:ArchiveClassesAtExit=application.jsa \ +-Dspring.context.exit=onRefreshed \ +-Dspring.profiles.active=standalone,datadir,offline \ +-Dgeosever.backend.data-directory.location=/tmp/tmpdatadir \ +org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/apps/geoserver/webui/Dockerfile b/src/apps/geoserver/webui/Dockerfile index 35f9293be..4cb66258d 100644 --- a/src/apps/geoserver/webui/Dockerfile +++ b/src/apps/geoserver/webui/Dockerfile @@ -20,3 +20,15 @@ COPY --from=builder spring-boot-loader/ ./ #see https://github.com/moby/moby/issues/37965 RUN true COPY --from=builder application/ ./ + +# Execute the CDS training run +RUN mkdir /tmp/tmpdatadir +RUN java \ +-XX:ArchiveClassesAtExit=application.jsa \ +-Dspring.context.exit=onRefreshed \ +-Dspring.profiles.active=standalone,datadir,offline \ +-Dgeosever.backend.data-directory.location=/tmp/tmpdatadir \ +org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/apps/geoserver/wfs/Dockerfile b/src/apps/geoserver/wfs/Dockerfile index 35f9293be..4cb66258d 100644 --- a/src/apps/geoserver/wfs/Dockerfile +++ b/src/apps/geoserver/wfs/Dockerfile @@ -20,3 +20,15 @@ COPY --from=builder spring-boot-loader/ ./ #see https://github.com/moby/moby/issues/37965 RUN true COPY --from=builder application/ ./ + +# Execute the CDS training run +RUN mkdir /tmp/tmpdatadir +RUN java \ +-XX:ArchiveClassesAtExit=application.jsa \ +-Dspring.context.exit=onRefreshed \ +-Dspring.profiles.active=standalone,datadir,offline \ +-Dgeosever.backend.data-directory.location=/tmp/tmpdatadir \ +org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/apps/geoserver/wms/Dockerfile b/src/apps/geoserver/wms/Dockerfile index 35f9293be..4cb66258d 100644 --- a/src/apps/geoserver/wms/Dockerfile +++ b/src/apps/geoserver/wms/Dockerfile @@ -20,3 +20,15 @@ COPY --from=builder spring-boot-loader/ ./ #see https://github.com/moby/moby/issues/37965 RUN true COPY --from=builder application/ ./ + +# Execute the CDS training run +RUN mkdir /tmp/tmpdatadir +RUN java \ +-XX:ArchiveClassesAtExit=application.jsa \ +-Dspring.context.exit=onRefreshed \ +-Dspring.profiles.active=standalone,datadir,offline \ +-Dgeosever.backend.data-directory.location=/tmp/tmpdatadir \ +org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/apps/geoserver/wps/Dockerfile b/src/apps/geoserver/wps/Dockerfile index 35f9293be..4cb66258d 100644 --- a/src/apps/geoserver/wps/Dockerfile +++ b/src/apps/geoserver/wps/Dockerfile @@ -20,3 +20,15 @@ COPY --from=builder spring-boot-loader/ ./ #see https://github.com/moby/moby/issues/37965 RUN true COPY --from=builder application/ ./ + +# Execute the CDS training run +RUN mkdir /tmp/tmpdatadir +RUN java \ +-XX:ArchiveClassesAtExit=application.jsa \ +-Dspring.context.exit=onRefreshed \ +-Dspring.profiles.active=standalone,datadir,offline \ +-Dgeosever.backend.data-directory.location=/tmp/tmpdatadir \ +org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/apps/infrastructure/admin/Dockerfile b/src/apps/infrastructure/admin/Dockerfile index bb8b79e07..8c40e34c1 100644 --- a/src/apps/infrastructure/admin/Dockerfile +++ b/src/apps/infrastructure/admin/Dockerfile @@ -28,3 +28,12 @@ HEALTHCHECK \ CMD curl -f -s -o /dev/null localhost:8080/actuator/health || exit 1 CMD exec env USER_ID="$(id -u)" USER_GID="$(id -g)" java $JAVA_OPTS org.springframework.boot.loader.JarLauncher + +# Execute the CDS training run +RUN java -XX:ArchiveClassesAtExit=application.jsa \ +-Dspring.context.exit=onRefreshed \ +-Dspring.profiles.active=standalone,offline \ +org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/apps/infrastructure/config/Dockerfile b/src/apps/infrastructure/config/Dockerfile index 6a5fadd53..80956b47b 100644 --- a/src/apps/infrastructure/config/Dockerfile +++ b/src/apps/infrastructure/config/Dockerfile @@ -21,7 +21,7 @@ RUN true COPY --from=builder application/ ./ # Either 'git' or 'native'. -ENV SPRING_PROFILES_ACTIVE=native +ENV SPRING_PROFILES_ACTIVE=native,standalone # 'native' profile config, use the default config embedded in the Docker image. # Feel free to override with a mounted volume ENV CONFIG_NATIVE_PATH=/etc/geoserver @@ -36,4 +36,8 @@ ENV CONFIG_GIT_BASEDIR: /tmp/git_config # avoid stack trace due to jgit not being able of creating a .config dir at $HOME ENV XDG_CONFIG_HOME: /tmp +# Execute the CDS training run +RUN java -XX:ArchiveClassesAtExit=application.jsa -Dspring.context.exit=onRefreshed org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/apps/infrastructure/discovery/Dockerfile b/src/apps/infrastructure/discovery/Dockerfile index 7a3d9206f..64efd18c7 100644 --- a/src/apps/infrastructure/discovery/Dockerfile +++ b/src/apps/infrastructure/discovery/Dockerfile @@ -19,3 +19,12 @@ COPY --from=builder spring-boot-loader/ ./ #see https://github.com/moby/moby/issues/37965 RUN true COPY --from=builder application/ ./ + +# Execute the CDS training run +RUN java -XX:ArchiveClassesAtExit=application.jsa \ +-Dspring.context.exit=onRefreshed \ +-Dspring.profiles.active=standalone \ +org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/apps/infrastructure/gateway/Dockerfile b/src/apps/infrastructure/gateway/Dockerfile index 7a3d9206f..64efd18c7 100644 --- a/src/apps/infrastructure/gateway/Dockerfile +++ b/src/apps/infrastructure/gateway/Dockerfile @@ -19,3 +19,12 @@ COPY --from=builder spring-boot-loader/ ./ #see https://github.com/moby/moby/issues/37965 RUN true COPY --from=builder application/ ./ + +# Execute the CDS training run +RUN java -XX:ArchiveClassesAtExit=application.jsa \ +-Dspring.context.exit=onRefreshed \ +-Dspring.profiles.active=standalone \ +org.springframework.boot.loader.JarLauncher +RUN rm -rf /tmp/* + +ENV JAVA_TOOL_OPTIONS="${DEFAULT_JAVA_TOOL_OPTIONS} -XX:SharedArchiveFile=application.jsa" diff --git a/src/starters/spring-boot/src/main/java/org/geoserver/cloud/app/ExitOnApplicationEventAutoConfiguration.java b/src/starters/spring-boot/src/main/java/org/geoserver/cloud/app/ExitOnApplicationEventAutoConfiguration.java new file mode 100644 index 000000000..989882ea6 --- /dev/null +++ b/src/starters/spring-boot/src/main/java/org/geoserver/cloud/app/ExitOnApplicationEventAutoConfiguration.java @@ -0,0 +1,122 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.app; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ApplicationContextEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.env.Environment; + +/** + * Allows to pass a JVM argument to exit the application upon specific {@link + * ApplicationContextEvent application events}, mostly useful to start up an application during the + * Docker image build process to create the AppCDS archive. + * + *

Usage: run the application with {@code -Dspring.context.exit=}, where {@code } + * is one of + * + *

    + *
  • {@link ExitOn#onPrepared onPrepared} + *
  • {@link ExitOn#onRefreshed onRefreshed} + *
  • {@link ExitOn#onStarted onStarted} + *
  • {@link ExitOn#onReady onReady} + *
+ * + *

Note Spring Boot 3.2 supports {@code spring.context.exit=onRefresh} as of this + * commit, and when we migrate from Spring Boot 2.7 to 3.2+ this will not be necessary most + * probably, although we've added additional events because some applications may fail to start + * without all the machinery in place at different stages. Nonetheless, the new {@code offline} + * embedded spring profile should allow them all to start without spring cloud bus, ACL, etc. + * + * @since 1.9.0 + */ +@AutoConfiguration +@ConditionalOnProperty("spring.context.exit") +@Slf4j +public class ExitOnApplicationEventAutoConfiguration { + + public enum ExitOn { + /** + * The {@link SpringApplication} is starting up and the {@link ApplicationContext} is fully + * prepared but not refreshed. The bean definitions will be loaded and the {@link + * Environment} is ready for use at this stage. + * + * @see ApplicationPreparedEvent + */ + onPrepared, + /** + * {@code ApplicationContext} gets initialized or refreshed + * + * @see ContextRefreshedEvent + */ + onRefreshed, + /** + * {@code ApplicationContext} has been refreshed but before any {@link ApplicationRunner + * application} and {@link CommandLineRunner command line} runners have been called. + * + * @see ApplicationStartedEvent + */ + onStarted, + /** + * Published as late as conceivably possible to indicate that the application is ready to + * service requests. The source of the event is the {@link SpringApplication} itself, but + * beware all initialization steps will have been completed by then. + * + * @see ApplicationReadyEvent + */ + onReady + } + + @Autowired private ApplicationContext appContext; + + @Value("${spring.context.exit}") + ExitOn exitOn; + + @EventListener(ApplicationPreparedEvent.class) + void exitOnApplicationPreparedEvent(ApplicationPreparedEvent event) { + exit(ExitOn.onStarted, event.getApplicationContext()); + } + + @EventListener(ContextRefreshedEvent.class) + void exitOnContextRefreshedEvent(ContextRefreshedEvent event) { + exit(ExitOn.onRefreshed, event.getApplicationContext()); + } + + @EventListener(ApplicationStartedEvent.class) + void exitOnApplicationStartedEvent(ApplicationStartedEvent event) { + exit(ExitOn.onStarted, event.getApplicationContext()); + } + + @EventListener(ApplicationReadyEvent.class) + void exitOnApplicationReadyEvent(ApplicationReadyEvent event) { + exit(ExitOn.onStarted, event.getApplicationContext()); + } + + private void exit(ExitOn ifGiven, ApplicationContext applicationContext) { + if (this.exitOn == ifGiven && applicationContext == this.appContext) { + log.warn("Exiting application, spring.context.exit={}", ifGiven); + try { + ((ConfigurableApplicationContext) applicationContext).close(); + } finally { + System.exit(0); + } + } + } +} diff --git a/src/starters/spring-boot/src/main/resources/META-INF/spring.factories b/src/starters/spring-boot/src/main/resources/META-INF/spring.factories index 7e12ee77a..abc412472 100644 --- a/src/starters/spring-boot/src/main/resources/META-INF/spring.factories +++ b/src/starters/spring-boot/src/main/resources/META-INF/spring.factories @@ -1,3 +1,4 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.geoserver.cloud.app.StartupLoggerAutoConfiguration,\ -org.geoserver.cloud.app.ServiceIdFilterAutoConfiguration \ No newline at end of file +org.geoserver.cloud.app.ServiceIdFilterAutoConfiguration,\ +org.geoserver.cloud.app.ExitOnApplicationEventAutoConfiguration \ No newline at end of file diff --git a/src/starters/spring-boot/src/main/resources/gs_cloud_bootstrap_profiles.yml b/src/starters/spring-boot/src/main/resources/gs_cloud_bootstrap_profiles.yml index f846e8328..86594bf7a 100644 --- a/src/starters/spring-boot/src/main/resources/gs_cloud_bootstrap_profiles.yml +++ b/src/starters/spring-boot/src/main/resources/gs_cloud_bootstrap_profiles.yml @@ -116,6 +116,17 @@ eureka: defaultZone: ${eureka.server.url} healthcheck: enabled: false # must only be set to true in application.yml, not bootstrap + +--- +spring.config.activate.on-profile: offline +spring: + cloud.config.enabled: false + cloud.config.discovery.enabled: false + cloud.discovery.enabled: false + cloud.bus.enabled: false + eureka.client.enabled: false + +geoserver.acl.enabled: false --- spring.config.activate.on-profile: local # Profile used for local development, so an app launched from the IDE can participate in the cluster.