001/*
002 * Copyright 2017-2026 original authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.micronaut.maven.jdkaotcache;
017
018import io.micronaut.core.annotation.Internal;
019import org.apache.maven.artifact.Artifact;
020import org.apache.maven.plugin.MojoExecutionException;
021import org.apache.maven.plugin.logging.Log;
022import org.jspecify.annotations.Nullable;
023
024import java.util.Collection;
025import java.util.List;
026
027/**
028 * The JDK AOT cache training run of an application: the mode, and what ends the run. It is resolved from
029 * {@code micronaut.docker.jdkAotCache.trainingMode}, the training paths and what the application's Micronaut version can
030 * do.
031 *
032 * <p>Without a configured mode, the run is a {@code LOAD} run if the Micronaut version has that mode, because it creates
033 * no bean and so needs none of the services the beans use, which an image build does not have. Otherwise, it starts the
034 * application, as every Micronaut version can.</p>
035 *
036 * @author Álvaro Sánchez-Mariscal
037 * @since 5.1.0
038 */
039@Internal
040public final class TrainingRun {
041
042    private static final String SCRIPT_LOAD = "load";
043    private static final String SCRIPT_SWITCH = "switch";
044    private static final String SCRIPT_SIGTERM = "sigterm";
045    private static final String LOG_PREFIX = "JDK AOT cache: ";
046    private static final String PATHS_OPTION = "micronaut.docker.jdkAotCache.trainingPaths";
047    private static final String LOAD_OPTION = TrainingMode.LOAD.option();
048    private static final String START_OPTION = TrainingMode.START.option();
049    private static final String LOAD_SENDS_NO_REQUESTS = "a LOAD training run does not start the application, so it "
050        + "sends no requests.";
051
052    private final TrainingMode mode;
053    private final boolean usesSwitch;
054    private final String description;
055    private final @Nullable String warning;
056
057    /**
058     * @param mode how far the application goes
059     * @param usesSwitch whether the Micronaut training-run switch ends the run. Otherwise, the training script warms the
060     * application up and stops it with SIGTERM
061     * @param description what the run does and why, for the build log
062     * @param warning what the build has to change before the application moves to a Micronaut version with the load
063     * mode, or {@code null} if nothing
064     */
065    TrainingRun(TrainingMode mode, boolean usesSwitch, String description, @Nullable String warning) {
066        this.mode = mode;
067        this.usesSwitch = usesSwitch;
068        this.description = description;
069        this.warning = warning;
070    }
071
072    /**
073     * A training run with nothing to warn about.
074     *
075     * @param mode how far the application goes
076     * @param usesSwitch whether the Micronaut training-run switch ends the run
077     * @param description what the run does and why, for the build log
078     */
079    TrainingRun(TrainingMode mode, boolean usesSwitch, String description) {
080        this(mode, usesSwitch, description, null);
081    }
082
083    /**
084     * @param configured the configured {@code micronaut.docker.jdkAotCache.trainingMode}, or {@code null} if it is not
085     * set
086     * @param trainingPaths the validated training paths
087     * @param artifacts the resolved dependencies of the application
088     * @return the training run
089     * @throws MojoExecutionException if the application's Micronaut version cannot run the mode, or if training paths
090     * are set for a run that does not start the application
091     */
092    public static TrainingRun resolve(@Nullable TrainingMode configured, List<String> trainingPaths,
093                                      Collection<Artifact> artifacts) throws MojoExecutionException {
094        boolean warmUp = !trainingPaths.isEmpty();
095        boolean loadAvailable = TrainingRunSwitch.hasLoadMode(artifacts);
096        if (configured == TrainingMode.LOAD) {
097            return configuredLoad(warmUp, loadAvailable);
098        }
099        if (configured == null && loadAvailable) {
100            if (warmUp) {
101                throw new MojoExecutionException(PATHS_OPTION + " is set, but the training mode is LOAD, the default "
102                    + "with this Micronaut version: " + LOAD_SENDS_NO_REQUESTS + " Set " + START_OPTION + " if the "
103                    + "application can start in the image build, or remove the training paths.");
104            }
105            return new TrainingRun(TrainingMode.LOAD, true, "training mode LOAD, the default: the application loads "
106                + "its bean definitions and exits without starting, so the training needs none of the services that "
107                + "its beans use. If the application can start in the image build, " + START_OPTION + " trains a more "
108                + "complete cache");
109        }
110        boolean usesSwitch = TrainingRunSwitch.isAvailable(artifacts, warmUp);
111        if (configured != null) {
112            return new TrainingRun(TrainingMode.START, usesSwitch, "training mode START: the training run starts the "
113                + "application");
114        }
115        // The build that sets training paths and no mode is the one that fails after a Micronaut upgrade
116        String warning = warmUp ? PATHS_OPTION + " is set and " + TrainingMode.PARAMETER + " is not. This build will fail "
117            + "once the application uses a Micronaut version with the LOAD training mode: LOAD becomes the default, "
118            + "and " + LOAD_SENDS_NO_REQUESTS + " Set " + START_OPTION + " now to keep starting the application" : null;
119        return new TrainingRun(TrainingMode.START, usesSwitch, "training mode START: the training run starts the "
120            + "application, because its Micronaut version has no training mode that loads it without starting it ("
121            + TrainingRunSwitch.MODE_PROPERTY + "). The services it needs at start-up must be reachable from the image "
122            + "build. With a Micronaut version that has that mode, LOAD becomes the default: set " + START_OPTION
123            + " to keep starting the application", warning);
124    }
125
126    /**
127     * An explicit {@code LOAD} fails on a Micronaut version without the mode, instead of starting an application that
128     * was declared unable to start. That is checked before the training paths, and its message covers them, so that
129     * a build with both problems is told about both at once.
130     */
131    private static TrainingRun configuredLoad(boolean warmUp, boolean loadAvailable) throws MojoExecutionException {
132        if (!loadAvailable) {
133            throw new MojoExecutionException(LOAD_OPTION + " needs a Micronaut version with the LOAD training mode ("
134                + TrainingRunSwitch.MODE_PROPERTY + "), which the application's Micronaut version does not have. "
135                + "Upgrade Micronaut, or remove the option: the training run then starts the application."
136                + (warmUp ? " If you upgrade, remove " + PATHS_OPTION + " as well: " + LOAD_SENDS_NO_REQUESTS : ""));
137        }
138        if (warmUp) {
139            throw new MojoExecutionException(PATHS_OPTION + " cannot be used with " + LOAD_OPTION + ": "
140                + LOAD_SENDS_NO_REQUESTS + " Remove the training paths, or set " + START_OPTION + " if the application "
141                + "can start in the image build.");
142        }
143        return new TrainingRun(TrainingMode.LOAD, true, "training mode LOAD: the application loads its bean "
144            + "definitions and exits without starting");
145    }
146
147    /**
148     * @return how far the application goes
149     */
150    TrainingMode mode() {
151        return mode;
152    }
153
154    /**
155     * @return whether the Micronaut training-run switch ends the run. Otherwise, the training script warms the
156     * application up and stops it with SIGTERM
157     */
158    public boolean usesSwitch() {
159        return usesSwitch;
160    }
161
162    /**
163     * @return what the run does and why, for the build log
164     */
165    String description() {
166        return description;
167    }
168
169    /**
170     * @return what the build has to change before the application moves to a Micronaut version with the load mode, or
171     * {@code null} if nothing
172     */
173    @Nullable String warning() {
174        return warning;
175    }
176
177    /**
178     * Writes what the run does and why to the build log, and the warning, if there is one.
179     *
180     * @param log the Maven log
181     */
182    public void log(Log log) {
183        log.info(LOG_PREFIX + description);
184        if (warning != null) {
185            log.warn(LOG_PREFIX + warning);
186        }
187    }
188
189    /**
190     * Writes what {@link #log(Log)} writes and, for a run that starts the application, how the training script of the
191     * generated Dockerfile ends it.
192     *
193     * @param log the Maven log
194     */
195    public void logGeneratedDockerfileRun(Log log) {
196        log(log);
197        if (mode == TrainingMode.START) {
198            log.info(LOG_PREFIX + (usesSwitch
199                ? "the application warms itself up and exits (Micronaut training-run switch)"
200                : "the training script warms the application up and stops it with SIGTERM"));
201        }
202    }
203
204    /**
205     * @param trainingPaths the validated training paths
206     * @return the Java system properties of the training JVM: those of the Micronaut training-run switch when it ends
207     * the run, none when the training script does
208     */
209    List<String> systemProperties(List<String> trainingPaths) {
210        return usesSwitch ? TrainingRunSwitch.systemProperties(mode, trainingPaths) : List.of();
211    }
212
213    /**
214     * @return the argument that tells the training script how to run the application: {@code load}, {@code switch} or
215     * {@code sigterm}
216     */
217    public String scriptArgument() {
218        if (mode == TrainingMode.LOAD) {
219            return SCRIPT_LOAD;
220        }
221        return usesSwitch ? SCRIPT_SWITCH : SCRIPT_SIGTERM;
222    }
223}