View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.lifecycle.internal.concurrent;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.xml.stream.XMLStreamException;
24  
25  import java.io.IOException;
26  import java.time.Duration;
27  import java.time.Instant;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.LinkedHashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Optional;
35  import java.util.Set;
36  import java.util.concurrent.ConcurrentHashMap;
37  import java.util.concurrent.ExecutionException;
38  import java.util.concurrent.Executors;
39  import java.util.concurrent.locks.ReadWriteLock;
40  import java.util.concurrent.locks.ReentrantReadWriteLock;
41  import java.util.stream.Collectors;
42  import java.util.stream.Stream;
43  
44  import org.apache.maven.api.Lifecycle;
45  import org.apache.maven.api.MonotonicClock;
46  import org.apache.maven.api.services.LifecycleRegistry;
47  import org.apache.maven.api.services.MavenException;
48  import org.apache.maven.api.xml.XmlNode;
49  import org.apache.maven.api.xml.XmlService;
50  import org.apache.maven.execution.BuildFailure;
51  import org.apache.maven.execution.BuildSuccess;
52  import org.apache.maven.execution.ExecutionEvent;
53  import org.apache.maven.execution.MavenExecutionRequest;
54  import org.apache.maven.execution.MavenSession;
55  import org.apache.maven.execution.ProjectDependencyGraph;
56  import org.apache.maven.execution.ProjectExecutionEvent;
57  import org.apache.maven.execution.ProjectExecutionListener;
58  import org.apache.maven.impl.util.PhasingExecutor;
59  import org.apache.maven.internal.MultilineMessageHelper;
60  import org.apache.maven.internal.impl.DefaultLifecycleRegistry;
61  import org.apache.maven.internal.transformation.TransformerManager;
62  import org.apache.maven.lifecycle.LifecycleExecutionException;
63  import org.apache.maven.lifecycle.LifecycleNotFoundException;
64  import org.apache.maven.lifecycle.LifecyclePhaseNotFoundException;
65  import org.apache.maven.lifecycle.MojoExecutionConfigurator;
66  import org.apache.maven.lifecycle.internal.BuildThreadFactory;
67  import org.apache.maven.lifecycle.internal.CompoundProjectExecutionListener;
68  import org.apache.maven.lifecycle.internal.ExecutionEventCatapult;
69  import org.apache.maven.lifecycle.internal.GoalTask;
70  import org.apache.maven.lifecycle.internal.LifecycleTask;
71  import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
72  import org.apache.maven.lifecycle.internal.MojoExecutor;
73  import org.apache.maven.lifecycle.internal.ReactorBuildStatus;
74  import org.apache.maven.lifecycle.internal.ReactorContext;
75  import org.apache.maven.lifecycle.internal.Task;
76  import org.apache.maven.lifecycle.internal.TaskSegment;
77  import org.apache.maven.model.Plugin;
78  import org.apache.maven.model.PluginExecution;
79  import org.apache.maven.plugin.MavenPluginManager;
80  import org.apache.maven.plugin.MojoExecution;
81  import org.apache.maven.plugin.MojoNotFoundException;
82  import org.apache.maven.plugin.PluginDescriptorParsingException;
83  import org.apache.maven.plugin.descriptor.MojoDescriptor;
84  import org.apache.maven.plugin.descriptor.Parameter;
85  import org.apache.maven.plugin.descriptor.PluginDescriptor;
86  import org.apache.maven.project.MavenProject;
87  import org.codehaus.plexus.classworlds.realm.ClassRealm;
88  import org.eclipse.aether.repository.RemoteRepository;
89  import org.slf4j.Logger;
90  import org.slf4j.LoggerFactory;
91  
92  import static org.apache.maven.api.Lifecycle.AFTER;
93  import static org.apache.maven.api.Lifecycle.AT;
94  import static org.apache.maven.api.Lifecycle.BEFORE;
95  import static org.apache.maven.api.Lifecycle.Phase.PACKAGE;
96  import static org.apache.maven.api.Lifecycle.Phase.READY;
97  import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.CREATED;
98  import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.EXECUTED;
99  import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.FAILED;
100 import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.PLAN;
101 import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.PLANNING;
102 import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.SCHEDULED;
103 import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.SETUP;
104 import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.SKIPPED;
105 import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.TEARDOWN;
106 
107 /**
108  * Executes the Maven build plan in a concurrent manner, handling the lifecycle phases and plugin executions.
109  * This executor implements a weave-mode build strategy, where builds are executed phase-by-phase rather than
110  * project-by-project.
111  *
112  * <h2>Key Features:</h2>
113  * <ul>
114  *   <li>Concurrent execution of compatible build steps across projects</li>
115  *   <li>Thread-safety validation for plugins</li>
116  *   <li>Support for forked executions and lifecycle phases</li>
117  *   <li>Dynamic build plan adjustment during execution</li>
118  * </ul>
119  *
120  * <h2>Execution Strategy:</h2>
121  * <p>The executor follows these main steps:</p>
122  * <ol>
123  *   <li>Initial plan creation based on project dependencies and task segments</li>
124  *   <li>Concurrent execution of build steps while maintaining dependency order</li>
125  *   <li>Dynamic replanning when necessary (e.g., for forked executions)</li>
126  *   <li>Project setup, execution, and teardown phases management</li>
127  * </ol>
128  *
129  * <h2>Thread Management:</h2>
130  * <p>The number of threads used is determined by:</p>
131  * <pre>
132  * min(degreeOfConcurrency, numberOfProjects)
133  * </pre>
134  * where degreeOfConcurrency is set via the -T command-line option.
135  *
136  * <h2>Build Step States:</h2>
137  * <ul>
138  *   <li>CREATED: Initial state of a build step</li>
139  *   <li>PLANNING: Step is being planned</li>
140  *   <li>SCHEDULED: Step is queued for execution</li>
141  *   <li>EXECUTED: Step has completed successfully</li>
142  *   <li>FAILED: Step execution failed</li>
143  * </ul>
144  *
145  * <p><strong>NOTE:</strong> This class is not part of any public API and can be changed or deleted without prior notice.</p>
146  *
147  * @since 3.0
148  */
149 @Named
150 public class BuildPlanExecutor {
151 
152     private static final Object GLOBAL = new Object();
153 
154     private final Logger logger = LoggerFactory.getLogger(getClass());
155 
156     private final MojoExecutor mojoExecutor;
157     private final ExecutionEventCatapult eventCatapult;
158     private final ProjectExecutionListener projectExecutionListener;
159     private final TransformerManager transformerManager;
160     private final BuildPlanLogger buildPlanLogger;
161     private final Map<String, MojoExecutionConfigurator> mojoExecutionConfigurators;
162     private final MavenPluginManager mavenPluginManager;
163     private final MojoDescriptorCreator mojoDescriptorCreator;
164     private final LifecycleRegistry lifecycles;
165 
166     @Inject
167     @SuppressWarnings("checkstyle:ParameterNumber")
168     public BuildPlanExecutor(
169             @Named("concurrent") MojoExecutor mojoExecutor,
170             ExecutionEventCatapult eventCatapult,
171             List<ProjectExecutionListener> listeners,
172             TransformerManager transformerManager,
173             BuildPlanLogger buildPlanLogger,
174             Map<String, MojoExecutionConfigurator> mojoExecutionConfigurators,
175             MavenPluginManager mavenPluginManager,
176             MojoDescriptorCreator mojoDescriptorCreator,
177             LifecycleRegistry lifecycles) {
178         this.mojoExecutor = mojoExecutor;
179         this.eventCatapult = eventCatapult;
180         this.projectExecutionListener = new CompoundProjectExecutionListener(listeners);
181         this.transformerManager = transformerManager;
182         this.buildPlanLogger = buildPlanLogger;
183         this.mojoExecutionConfigurators = mojoExecutionConfigurators;
184         this.mavenPluginManager = mavenPluginManager;
185         this.mojoDescriptorCreator = mojoDescriptorCreator;
186         this.lifecycles = lifecycles;
187     }
188 
189     public void execute(MavenSession session, ReactorContext reactorContext, List<TaskSegment> taskSegments)
190             throws ExecutionException, InterruptedException {
191         try (BuildContext ctx = new BuildContext(session, reactorContext, taskSegments)) {
192             ctx.execute();
193         }
194     }
195 
196     class BuildContext implements AutoCloseable {
197         final MavenSession session;
198         final ReactorContext reactorContext;
199         final PhasingExecutor executor;
200         final Map<Object, Clock> clocks = new ConcurrentHashMap<>();
201         final ReadWriteLock lock = new ReentrantReadWriteLock();
202         final int threads;
203         BuildPlan plan;
204 
205         BuildContext(MavenSession session, ReactorContext reactorContext, List<TaskSegment> taskSegments) {
206             this.session = session;
207             this.reactorContext = reactorContext;
208             this.threads = Math.min(
209                     session.getRequest().getDegreeOfConcurrency(),
210                     session.getProjects().size());
211             // Propagate the parallel flag to the root session
212             session.setParallel(threads > 1);
213             this.executor = new PhasingExecutor(Executors.newFixedThreadPool(threads, new BuildThreadFactory()));
214 
215             // build initial plan
216             this.plan = buildInitialPlan(taskSegments);
217         }
218 
219         BuildContext() {
220             this.session = null;
221             this.reactorContext = null;
222             this.threads = 1;
223             this.executor = null;
224             this.plan = null;
225         }
226 
227         public BuildPlan buildInitialPlan(List<TaskSegment> taskSegments) {
228             int nThreads = Math.min(
229                     session.getRequest().getDegreeOfConcurrency(),
230                     session.getProjects().size());
231             boolean parallel = nThreads > 1;
232             // Propagate the parallel flag to the root session
233             session.setParallel(parallel);
234 
235             ProjectDependencyGraph dependencyGraph = session.getProjectDependencyGraph();
236             MavenProject rootProject = session.getTopLevelProject();
237 
238             Map<MavenProject, List<MavenProject>> allProjects = new LinkedHashMap<>();
239             dependencyGraph
240                     .getSortedProjects()
241                     .forEach(p -> allProjects.put(p, dependencyGraph.getUpstreamProjects(p, false)));
242 
243             BuildPlan plan = new BuildPlan(allProjects);
244             for (TaskSegment taskSegment : taskSegments) {
245                 Map<MavenProject, List<MavenProject>> projects = taskSegment.isAggregating()
246                         ? Collections.singletonMap(rootProject, allProjects.get(rootProject))
247                         : allProjects;
248 
249                 BuildPlan segment = calculateMojoExecutions(projects, taskSegment.getTasks());
250                 plan.then(segment);
251             }
252 
253             // Create plan, setup and teardown
254             for (MavenProject project : plan.getAllProjects().keySet()) {
255                 BuildStep pplan = new BuildStep(PLAN, project, null);
256                 pplan.status.set(PLANNING); // the plan step always need planning
257                 BuildStep setup = new BuildStep(SETUP, project, null);
258                 BuildStep teardown = new BuildStep(TEARDOWN, project, null);
259                 teardown.executeAfter(setup);
260                 setup.executeAfter(pplan);
261                 plan.steps(project).forEach(step -> {
262                     if (step.predecessors.stream().noneMatch(s -> s.project == project)) {
263                         step.executeAfter(setup);
264                     } else if (step.successors.stream().noneMatch(s -> s.project == project)) {
265                         teardown.executeAfter(step);
266                     }
267                 });
268                 Stream.of(pplan, setup, teardown).forEach(step -> plan.addStep(project, step.name, step));
269             }
270 
271             return plan;
272         }
273 
274         private void checkUnboundVersions(BuildPlan buildPlan) {
275             String defaulModelId = DefaultLifecycleRegistry.DEFAULT_LIFECYCLE_MODELID;
276             List<String> unversionedPlugins = buildPlan
277                     .allSteps()
278                     .flatMap(step -> step.mojos.values().stream().flatMap(map -> map.values().stream()))
279                     .map(MojoExecution::getPlugin)
280                     .filter(p -> p.getLocation("version") != null
281                             && p.getLocation("version").getSource() != null
282                             && defaulModelId.equals(
283                                     p.getLocation("version").getSource().getModelId()))
284                     .distinct()
285                     .map(Plugin::getArtifactId) // managed by us, groupId is always o.a.m.plugins
286                     .toList();
287             if (!unversionedPlugins.isEmpty()) {
288                 logger.warn("Version not locked for default bindings plugins " + unversionedPlugins
289                         + ", you should define versions in pluginManagement section of your " + "pom.xml or parent");
290             }
291         }
292 
293         private void checkThreadSafety(BuildPlan buildPlan) {
294             if (threads > 1) {
295                 Set<MojoExecution> unsafeExecutions = buildPlan
296                         .allSteps()
297                         .flatMap(step -> step.mojos.values().stream().flatMap(map -> map.values().stream()))
298                         .filter(execution -> !execution.getMojoDescriptor().isV4Api())
299                         .collect(Collectors.toSet());
300                 if (!unsafeExecutions.isEmpty()) {
301                     for (String s : MultilineMessageHelper.format(
302                             """
303                                 Your build is requesting concurrent execution, but this project contains the \
304                                 following plugin(s) that have goals not built with Maven 4 to support concurrent \
305                                 execution. While this /may/ work fine, please look for plugin updates and/or \
306                                 request plugins be made thread-safe. If reporting an issue, report it against the \
307                                 plugin in question, not against Apache Maven.""")) {
308                         logger.warn(s);
309                     }
310                     if (logger.isDebugEnabled()) {
311                         Set<MojoDescriptor> unsafeGoals = unsafeExecutions.stream()
312                                 .map(MojoExecution::getMojoDescriptor)
313                                 .collect(Collectors.toSet());
314                         logger.warn("The following goals are not Maven 4 goals:");
315                         for (MojoDescriptor unsafeGoal : unsafeGoals) {
316                             logger.warn("  " + unsafeGoal.getId());
317                         }
318                     } else {
319                         Set<Plugin> unsafePlugins = unsafeExecutions.stream()
320                                 .map(MojoExecution::getPlugin)
321                                 .collect(Collectors.toSet());
322                         logger.warn("The following plugins are not Maven 4 plugins:");
323                         for (Plugin unsafePlugin : unsafePlugins) {
324                             logger.warn("  " + unsafePlugin.getId());
325                         }
326                         logger.warn("");
327                         logger.warn("Enable verbose output (-X) to see precisely which goals are not marked as"
328                                 + " thread-safe.");
329                     }
330                     logger.warn(MultilineMessageHelper.separatorLine());
331                 }
332             }
333         }
334 
335         void execute() {
336             try (var phase = executor.phase()) {
337                 plan();
338                 executePlan();
339             } catch (Exception e) {
340                 session.getResult().addException(e);
341             }
342         }
343 
344         @Override
345         public void close() {
346             this.executor.close();
347         }
348 
349         /**
350          * Processes a single build step, deciding whether to schedule it for execution or skip it.
351          *
352          * @param step The build step to process
353          */
354         private void processStep(BuildStep step) {
355             // 1. Apply reactor failure behavior to decide whether to schedule or skip
356             ReactorBuildStatus status = reactorContext.getReactorBuildStatus();
357             boolean isAfterStep = step.name.startsWith(AFTER);
358             boolean shouldExecute;
359 
360             // Check if all predecessors are executed successfully
361             boolean allPredecessorsExecuted = step.predecessors.stream().allMatch(s -> s.status.get() == EXECUTED);
362 
363             // Special case for after:* steps - they should run if their corresponding before:* step ran
364             if (isAfterStep) {
365                 String phaseName = step.name.substring(AFTER.length());
366                 // Always process after:* steps for cleanup if their before:* step ran
367                 shouldExecute = plan.step(step.project, BEFORE + phaseName)
368                         .map(s -> {
369                             int stepStatus = s.status.get();
370                             return stepStatus == EXECUTED;
371                         })
372                         .orElse(false);
373 
374                 // Check if any predecessor failed - if so, we'll run the step but mark it as SKIPPED
375                 boolean anyPredecessorFailed = step.predecessors.stream().anyMatch(s -> s.status.get() == FAILED);
376 
377                 // If any predecessor failed, we'll use a special status transition: CREATED -> SKIPPED
378                 // This ensures the step runs for cleanup but is marked as skipped in the end
379                 if (shouldExecute && anyPredecessorFailed) {
380                     // We'll run the step but mark it as SKIPPED instead of SCHEDULED
381                     if (step.status.compareAndSet(CREATED, SKIPPED)) {
382                         logger.debug(
383                                 "Running after:* step {} for cleanup but marking it as SKIPPED because a predecessor failed",
384                                 step);
385                         executor.execute(() -> {
386                             try {
387                                 executeStep(step);
388                                 executePlan();
389                             } catch (Exception e) {
390                                 step.status.compareAndSet(SKIPPED, FAILED);
391                                 // Store the exception in the step for handling in the TEARDOWN phase
392                                 step.exception = e;
393                                 logger.debug("Stored exception for step {} to be handled in TEARDOWN phase", step, e);
394                                 // Let the scheduler handle after:* phases and TEARDOWN in the next cycle
395                                 executePlan();
396                             }
397                         });
398                         return; // Skip the rest of the method since we've handled this step
399                     }
400                 }
401             } else if (TEARDOWN.equals(step.name)) {
402                 // TEARDOWN should always run to ensure proper cleanup and error handling
403                 // We'll handle success/failure reporting inside the TEARDOWN phase
404                 shouldExecute = true;
405             } else {
406                 // For regular steps:
407                 // Don't run for halted builds, blacklisted projects, or if predecessors failed
408                 shouldExecute = !status.isHalted() && !status.isBlackListed(step.project) && allPredecessorsExecuted;
409             }
410 
411             // 2. Either schedule the step or mark it as skipped based on the decision
412             if (shouldExecute && step.status.compareAndSet(CREATED, SCHEDULED)) {
413                 boolean nextIsPlanning = step.successors.stream().anyMatch(st -> PLAN.equals(st.name));
414                 executor.execute(() -> {
415                     try {
416                         executeStep(step);
417                         if (nextIsPlanning) {
418                             lock.writeLock().lock();
419                             try {
420                                 plan();
421                             } finally {
422                                 lock.writeLock().unlock();
423                             }
424                         }
425                         executePlan();
426                     } catch (Exception e) {
427                         step.status.compareAndSet(SCHEDULED, FAILED);
428 
429                         // Store the exception in the step for handling in the TEARDOWN phase
430                         step.exception = e;
431                         logger.debug("Stored exception for step {} to be handled in TEARDOWN phase", step, e);
432 
433                         // Let the scheduler handle after:* phases and TEARDOWN in the next cycle
434                         executePlan();
435                     }
436                 });
437             } else if (step.status.compareAndSet(CREATED, SKIPPED)) {
438                 // Skip the step and provide a specific reason
439                 if (!shouldExecute) {
440                     if (status.isHalted()) {
441                         logger.debug("Skipping step {} because the build is halted", step);
442                     } else if (status.isBlackListed(step.project)) {
443                         logger.debug("Skipping step {} because the project is blacklisted", step);
444                     } else if (TEARDOWN.equals(step.name)) {
445                         // This should never happen given we always process TEARDOWN steps
446                         logger.warn("Unexpected skipping of TEARDOWN step {}", step);
447                     } else {
448                         logger.debug("Skipping step {} because a dependency has failed", step);
449                     }
450                 } else {
451                     // Skip because predecessors failed or were skipped
452                     logger.debug(
453                             "Skipping step {} because one or more predecessors did not execute successfully", step);
454                 }
455                 // Recursively call executePlan to process steps that depend on this one
456                 executePlan();
457             }
458         }
459 
460         private void executePlan() {
461             // Even if the build is halted, we still want to execute TEARDOWN and after:* steps
462             // for proper cleanup, so we don't return early here
463             Clock global = getClock(GLOBAL);
464             global.start();
465             lock.readLock().lock();
466             try {
467                 // Process build steps in a logical order:
468                 // 1. Find steps that are not yet started (CREATED status)
469                 // 2. Check if all their predecessors have completed (in a terminal state)
470                 // 3. Process each step (schedule or skip based on reactor failure behavior)
471                 plan.sortedNodes().stream()
472                         // 1. Filter steps that are in CREATED state
473                         .filter(BuildStep::isCreated)
474                         // 2. Check if all predecessors are in a terminal state
475                         .filter(step -> step.predecessors.stream().allMatch(BuildStep::isDone))
476                         // 3. Process each step
477                         .forEach(this::processStep);
478             } finally {
479                 lock.readLock().unlock();
480             }
481         }
482 
483         /**
484          * Executes a single build step, which can be one of:
485          * - PLAN: Project build planning
486          * - SETUP: Project initialization
487          * - TEARDOWN: Project cleanup
488          * - Default: Actual mojo/plugin executions
489          *
490          * @param step The build step to execute
491          * @throws IOException If there's an IO error during execution
492          * @throws LifecycleExecutionException If there's a lifecycle execution error
493          */
494         private void executeStep(BuildStep step) throws IOException, LifecycleExecutionException {
495             Clock clock = getClock(step.project);
496             switch (step.name) {
497                 case PLAN:
498                     // Planning steps should be executed out of normal execution
499                     throw new IllegalStateException();
500                 case SETUP:
501                     attachToThread(step);
502                     transformerManager.injectTransformedArtifacts(session.getRepositorySession(), step.project);
503                     projectExecutionListener.beforeProjectExecution(new ProjectExecutionEvent(session, step.project));
504                     eventCatapult.fire(ExecutionEvent.Type.ProjectStarted, session, null);
505                     break;
506                 case TEARDOWN:
507                     attachToThread(step);
508 
509                     // Check if there are any stored exceptions for this project
510                     List<Throwable> failures = null;
511                     boolean allStepsExecuted = true;
512                     for (BuildStep projectStep : plan.steps(step.project).toList()) {
513                         Exception exception = projectStep.exception;
514                         if (exception != null) {
515                             if (failures == null) {
516                                 failures = new ArrayList<>();
517                             }
518                             failures.add(exception);
519                         }
520                         allStepsExecuted &= step == projectStep || projectStep.status.get() == EXECUTED;
521                     }
522 
523                     if (failures != null) {
524                         // Handle the stored exception
525                         Throwable failure;
526                         if (failures.size() == 1) {
527                             failure = failures.get(
528                                     0); // Single exception, no need to wrap it in a LifecycleExecutionException
529                         } else {
530                             failure = new LifecycleExecutionException("Error building project");
531                             failures.forEach(failure::addSuppressed);
532                         }
533                         handleBuildError(reactorContext, session, step.project, failure);
534                     } else if (allStepsExecuted) {
535                         // If there were no failures, report success
536                         projectExecutionListener.afterProjectExecutionSuccess(
537                                 new ProjectExecutionEvent(session, step.project, Collections.emptyList()));
538                         reactorContext
539                                 .getResult()
540                                 .addBuildSummary(new BuildSuccess(step.project, clock.wallTime(), clock.execTime()));
541                         eventCatapult.fire(ExecutionEvent.Type.ProjectSucceeded, session, null);
542                     } else {
543                         eventCatapult.fire(ExecutionEvent.Type.ProjectSkipped, session, null);
544                     }
545                     break;
546                 default:
547                     List<MojoExecution> executions = step.executions().toList();
548                     if (!executions.isEmpty()) {
549                         attachToThread(step);
550                         clock.start();
551                         try {
552                             executions.forEach(mojoExecution -> {
553                                 mojoExecutionConfigurator(mojoExecution).configure(step.project, mojoExecution, true);
554                                 finalizeMojoConfiguration(mojoExecution);
555                             });
556                             mojoExecutor.execute(session, executions);
557                         } finally {
558                             clock.stop();
559                         }
560                     }
561                     break;
562             }
563             step.status.compareAndSet(SCHEDULED, EXECUTED);
564         }
565 
566         private void attachToThread(BuildStep step) {
567             BuildPlanExecutor.attachToThread(step.project);
568             session.setCurrentProject(step.project);
569         }
570 
571         private Clock getClock(Object key) {
572             return clocks.computeIfAbsent(key, p -> new Clock());
573         }
574 
575         private void plan() {
576             lock.writeLock().lock();
577             try {
578                 Set<BuildStep> planSteps = plan.allSteps()
579                         .filter(step -> PLAN.equals(step.name))
580                         .filter(step -> step.predecessors.stream().allMatch(s -> s.status.get() == EXECUTED))
581                         .filter(step -> step.status.compareAndSet(PLANNING, SCHEDULED))
582                         .collect(Collectors.toSet());
583                 for (BuildStep step : planSteps) {
584                     MavenProject project = step.project;
585                     for (Plugin plugin : project.getBuild().getPlugins()) {
586                         for (PluginExecution execution : plugin.getExecutions()) {
587                             for (String goal : execution.getGoals()) {
588                                 MojoDescriptor mojoDescriptor = getMojoDescriptor(project, plugin, goal);
589                                 String phase =
590                                         execution.getPhase() != null ? execution.getPhase() : mojoDescriptor.getPhase();
591                                 if (phase == null) {
592                                     continue;
593                                 }
594                                 String tmpResolvedPhase = plan.aliases().getOrDefault(phase, phase);
595                                 String resolvedPhase = tmpResolvedPhase.startsWith(AT)
596                                         ? tmpResolvedPhase.substring(AT.length())
597                                         : tmpResolvedPhase;
598                                 plan.step(project, resolvedPhase).ifPresent(n -> {
599                                     MojoExecution mojoExecution = new MojoExecution(mojoDescriptor, execution.getId());
600                                     mojoExecution.setLifecyclePhase(phase);
601                                     n.addMojo(mojoExecution, execution.getPriority());
602                                     if (mojoDescriptor.getDependencyCollectionRequired() != null
603                                             || mojoDescriptor.getDependencyResolutionRequired() != null) {
604                                         for (MavenProject p :
605                                                 plan.getAllProjects().get(project)) {
606                                             plan.step(p, AFTER + PACKAGE)
607                                                     .ifPresent(a -> plan.requiredStep(project, resolvedPhase)
608                                                             .executeAfter(a));
609                                         }
610                                     }
611                                 });
612                             }
613                         }
614                     }
615                 }
616 
617                 BuildPlan buildPlan = plan;
618                 for (BuildStep step :
619                         planSteps.stream().flatMap(p -> plan.steps(p.project)).toList()) {
620                     for (MojoExecution execution : step.executions().toList()) {
621                         buildPlan = computeForkPlan(step, execution, buildPlan);
622                     }
623                 }
624 
625                 for (BuildStep step : planSteps) {
626                     MavenProject project = step.project;
627                     buildPlanLogger.writePlan(plan, project);
628                     step.status.compareAndSet(SCHEDULED, EXECUTED);
629                 }
630 
631                 checkThreadSafety(plan);
632                 checkUnboundVersions(plan);
633             } finally {
634                 lock.writeLock().unlock();
635             }
636         }
637 
638         protected BuildPlan computeForkPlan(BuildStep step, MojoExecution execution, BuildPlan buildPlan) {
639             MojoDescriptor mojoDescriptor = execution.getMojoDescriptor();
640             PluginDescriptor pluginDescriptor = mojoDescriptor.getPluginDescriptor();
641             String forkedGoal = mojoDescriptor.getExecuteGoal();
642             String phase = mojoDescriptor.getExecutePhase();
643             // We have a fork goal
644             if (forkedGoal != null && !forkedGoal.isEmpty()) {
645                 MojoDescriptor forkedMojoDescriptor = pluginDescriptor.getMojo(forkedGoal);
646                 if (forkedMojoDescriptor == null) {
647                     throw new MavenException(new MojoNotFoundException(forkedGoal, pluginDescriptor));
648                 }
649 
650                 List<MavenProject> toFork = new ArrayList<>();
651                 toFork.add(step.project);
652                 if (mojoDescriptor.isAggregator() && step.project.getCollectedProjects() != null) {
653                     toFork.addAll(step.project.getCollectedProjects());
654                 }
655 
656                 BuildPlan plan = new BuildPlan();
657                 for (MavenProject project : toFork) {
658                     BuildStep st = new BuildStep(forkedGoal, project, null);
659                     MojoExecution mojoExecution = new MojoExecution(forkedMojoDescriptor, forkedGoal);
660                     st.addMojo(mojoExecution, 0);
661                     Map<String, BuildStep> n = new HashMap<>();
662                     n.put(forkedGoal, st);
663                     plan.addProject(project, n);
664                 }
665 
666                 for (BuildStep astep : plan.allSteps().toList()) {
667                     for (MojoExecution aexecution : astep.executions().toList()) {
668                         plan = computeForkPlan(astep, aexecution, plan);
669                     }
670                 }
671 
672                 return plan;
673 
674             } else if (phase != null && !phase.isEmpty()) {
675                 String forkedLifecycle = mojoDescriptor.getExecuteLifecycle();
676                 Lifecycle lifecycle;
677                 if (forkedLifecycle != null && !forkedLifecycle.isEmpty()) {
678                     org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle lifecycleOverlay;
679                     try {
680                         lifecycleOverlay = pluginDescriptor.getLifecycleMapping(forkedLifecycle);
681                     } catch (IOException | XMLStreamException e) {
682                         throw new MavenException(new PluginDescriptorParsingException(
683                                 pluginDescriptor.getPlugin(), pluginDescriptor.getSource(), e));
684                     }
685                     if (lifecycleOverlay == null) {
686                         Optional<Lifecycle> lf = lifecycles.lookup(forkedLifecycle);
687                         if (lf.isPresent()) {
688                             lifecycle = lf.get();
689                         } else {
690                             throw new MavenException(new LifecycleNotFoundException(forkedLifecycle));
691                         }
692                     } else {
693                         lifecycle = new PluginLifecycle(lifecycleOverlay, pluginDescriptor);
694                     }
695                 } else {
696                     if (execution.getLifecyclePhase() != null) {
697                         String n = execution.getLifecyclePhase();
698                         String phaseName = n.startsWith(BEFORE)
699                                 ? n.substring(BEFORE.length())
700                                 : n.startsWith(AFTER) ? n.substring(AFTER.length()) : n;
701                         lifecycle = lifecycles.stream()
702                                 .filter(l -> l.allPhases().anyMatch(p -> phaseName.equals(p.name())))
703                                 .findFirst()
704                                 .orElse(null);
705                         if (lifecycle == null) {
706                             throw new IllegalStateException();
707                         }
708                     } else {
709                         lifecycle = lifecycles.require(Lifecycle.DEFAULT);
710                     }
711                 }
712 
713                 String resolvedPhase = getResolvedPhase(lifecycle, phase);
714 
715                 Map<MavenProject, List<MavenProject>> map = Collections.singletonMap(
716                         step.project, plan.getAllProjects().get(step.project));
717                 BuildPlan forkedPlan = calculateLifecycleMappings(map, lifecycle, resolvedPhase);
718                 forkedPlan.then(buildPlan);
719                 return forkedPlan;
720             } else {
721                 return buildPlan;
722             }
723         }
724 
725         private String getResolvedPhase(Lifecycle lifecycle, String phase) {
726             return lifecycle.aliases().stream()
727                     .filter(a -> phase.equals(a.v3Phase()))
728                     .findFirst()
729                     .map(Lifecycle.Alias::v4Phase)
730                     .orElse(phase);
731         }
732 
733         private String getResolvedPhase(String phase) {
734             return lifecycles.stream()
735                     .flatMap(l -> l.aliases().stream())
736                     .filter(a -> phase.equals(a.v3Phase()))
737                     .findFirst()
738                     .map(Lifecycle.Alias::v4Phase)
739                     .orElse(phase);
740         }
741 
742         /**
743          * Handles build errors by recording the error, notifying listeners, and updating the ReactorBuildStatus
744          * based on the reactor failure behavior.
745          * <p>
746          * This method works in conjunction with the filtering in executePlan():
747          * - For FAIL_FAST: Sets ReactorBuildStatus to halted, which causes executePlan to only process after:* steps
748          * - For FAIL_AT_END: Blacklists the project and its dependents, which causes executePlan to skip them
749          * - For FAIL_NEVER: Does nothing special, allowing all projects to continue building
750          * <p>
751          * Note: TEARDOWN steps are not executed for failed or blacklisted projects, as they're designed for
752          * successful project completions.
753          *
754          * @param buildContext The reactor context
755          * @param session The Maven session
756          * @param mavenProject The project that failed
757          * @param t The exception that caused the failure
758          */
759         protected void handleBuildError(
760                 final ReactorContext buildContext,
761                 final MavenSession session,
762                 final MavenProject mavenProject,
763                 Throwable t) {
764             // record the error and mark the project as failed
765             Clock clock = getClock(mavenProject);
766             buildContext.getResult().addException(t);
767             buildContext
768                     .getResult()
769                     .addBuildSummary(new BuildFailure(mavenProject, clock.execTime(), clock.wallTime(), t));
770 
771             // notify listeners about "soft" project build failures only
772             if (t instanceof Exception exception && !(t instanceof RuntimeException)) {
773                 eventCatapult.fire(ExecutionEvent.Type.ProjectFailed, session, null, exception);
774             }
775 
776             // reactor failure modes
777             if (t instanceof RuntimeException || !(t instanceof Exception)) {
778                 // fail fast on RuntimeExceptions, Errors and "other" Throwables
779                 // assume these are system errors and further build is meaningless
780                 buildContext.getReactorBuildStatus().halt();
781             } else if (MavenExecutionRequest.REACTOR_FAIL_NEVER.equals(session.getReactorFailureBehavior())) {
782                 // continue the build
783             } else if (MavenExecutionRequest.REACTOR_FAIL_AT_END.equals(session.getReactorFailureBehavior())) {
784                 // continue the build but ban all projects that depend on the failed one
785                 buildContext.getReactorBuildStatus().blackList(mavenProject);
786             } else if (MavenExecutionRequest.REACTOR_FAIL_FAST.equals(session.getReactorFailureBehavior())) {
787                 buildContext.getReactorBuildStatus().halt();
788             } else {
789                 logger.error("invalid reactor failure behavior " + session.getReactorFailureBehavior());
790                 buildContext.getReactorBuildStatus().halt();
791             }
792         }
793 
794         public BuildPlan calculateMojoExecutions(Map<MavenProject, List<MavenProject>> projects, List<Task> tasks) {
795             BuildPlan buildPlan = new BuildPlan(projects);
796 
797             for (Task task : tasks) {
798                 BuildPlan step;
799 
800                 if (task instanceof GoalTask) {
801                     String pluginGoal = task.getValue();
802 
803                     String executionId = "default-cli";
804                     int executionIdx = pluginGoal.indexOf('@');
805                     if (executionIdx > 0) {
806                         executionId = pluginGoal.substring(executionIdx + 1);
807                     }
808 
809                     step = new BuildPlan();
810                     for (MavenProject project : projects.keySet()) {
811                         BuildStep st = new BuildStep(pluginGoal, project, null);
812                         MojoDescriptor mojoDescriptor = getMojoDescriptor(project, pluginGoal);
813                         MojoExecution mojoExecution =
814                                 new MojoExecution(mojoDescriptor, executionId, MojoExecution.Source.CLI);
815                         st.addMojo(mojoExecution, 0);
816                         Map<String, BuildStep> n = new HashMap<>();
817                         n.put(pluginGoal, st);
818                         step.addProject(project, n);
819                     }
820                 } else if (task instanceof LifecycleTask) {
821                     String lifecyclePhase = task.getValue();
822 
823                     step = calculateLifecycleMappings(projects, lifecyclePhase);
824 
825                 } else {
826                     throw new IllegalStateException("unexpected task " + task);
827                 }
828 
829                 buildPlan.then(step);
830             }
831 
832             return buildPlan;
833         }
834 
835         private MojoDescriptor getMojoDescriptor(MavenProject project, Plugin plugin, String goal) {
836             try {
837                 return mavenPluginManager.getMojoDescriptor(
838                         plugin, goal, project.getRemotePluginRepositories(), session.getRepositorySession());
839             } catch (MavenException e) {
840                 throw e;
841             } catch (Exception e) {
842                 throw new MavenException(e);
843             }
844         }
845 
846         private MojoDescriptor getMojoDescriptor(MavenProject project, String task) {
847             try {
848                 return mojoDescriptorCreator.getMojoDescriptor(task, session, project);
849             } catch (MavenException e) {
850                 throw e;
851             } catch (Exception e) {
852                 throw new MavenException(e);
853             }
854         }
855 
856         public BuildPlan calculateLifecycleMappings(
857                 Map<MavenProject, List<MavenProject>> projects, String lifecyclePhase) {
858 
859             String resolvedPhase = getResolvedPhase(lifecyclePhase);
860             String mainPhase = resolvedPhase.startsWith(BEFORE)
861                     ? resolvedPhase.substring(BEFORE.length())
862                     : resolvedPhase.startsWith(AFTER)
863                             ? resolvedPhase.substring(AFTER.length())
864                             : resolvedPhase.startsWith(AT) ? resolvedPhase.substring(AT.length()) : resolvedPhase;
865 
866             /*
867              * Determine the lifecycle that corresponds to the given phase.
868              */
869             Lifecycle lifecycle = lifecycles.stream()
870                     .filter(l -> l.allPhases().anyMatch(p -> mainPhase.equals(p.name())))
871                     .findFirst()
872                     .orElse(null);
873 
874             if (lifecycle == null) {
875                 throw new MavenException(new LifecyclePhaseNotFoundException(
876                         "Unknown lifecycle phase \"" + lifecyclePhase
877                                 + "\". You must specify a valid lifecycle phase"
878                                 + " or a goal in the format <plugin-prefix>:<goal> or"
879                                 + " <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: "
880                                 + lifecycles.stream()
881                                         .flatMap(l -> l.allPhases().map(Lifecycle.Phase::name))
882                                         .collect(Collectors.joining(", "))
883                                 + ".",
884                         lifecyclePhase));
885             }
886 
887             return calculateLifecycleMappings(projects, lifecycle, resolvedPhase);
888         }
889 
890         public BuildPlan calculateLifecycleMappings(
891                 Map<MavenProject, List<MavenProject>> projects, Lifecycle lifecycle, String lifecyclePhase) {
892             BuildPlan plan = new BuildPlan(projects);
893 
894             for (MavenProject project : projects.keySet()) {
895                 // For each phase, create and sequence the pre, run and post steps
896                 Map<String, BuildStep> steps = lifecycle
897                         .allPhases()
898                         .flatMap(phase -> {
899                             BuildStep a = new BuildStep(BEFORE + phase.name(), project, phase);
900                             BuildStep b = new BuildStep(phase.name(), project, phase);
901                             BuildStep c = new BuildStep(AFTER + phase.name(), project, phase);
902                             b.executeAfter(a);
903                             c.executeAfter(b);
904                             return Stream.of(a, b, c);
905                         })
906                         .collect(Collectors.toMap(n -> n.name, n -> n));
907                 // for each phase, make sure children phases are executed between before and after steps
908                 lifecycle.allPhases().forEach(phase -> phase.phases().forEach(child -> {
909                     steps.get(BEFORE + child.name()).executeAfter(steps.get(BEFORE + phase.name()));
910                     steps.get(AFTER + phase.name()).executeAfter(steps.get(AFTER + child.name()));
911                 }));
912                 // for each phase, create links between this project phases
913                 lifecycle.allPhases().forEach(phase -> {
914                     phase.links().stream()
915                             .filter(l -> l.pointer().type() == Lifecycle.Pointer.Type.PROJECT)
916                             .forEach(link -> {
917                                 String n1 = phase.name();
918                                 String n2 = link.pointer().phase();
919                                 if (link.kind() == Lifecycle.Link.Kind.AFTER) {
920                                     steps.get(BEFORE + n1).executeAfter(steps.get(AFTER + n2));
921                                 } else {
922                                     steps.get(BEFORE + n2).executeAfter(steps.get(AFTER + n1));
923                                 }
924                             });
925                 });
926 
927                 // Only keep mojo executions before the end phase
928                 String endPhase = lifecyclePhase.startsWith(BEFORE) || lifecyclePhase.startsWith(AFTER)
929                         ? lifecyclePhase
930                         : lifecyclePhase.startsWith(AT)
931                                 ? lifecyclePhase.substring(AT.length())
932                                 : AFTER + lifecyclePhase;
933                 Set<BuildStep> toKeep = steps.get(endPhase).allPredecessors().collect(Collectors.toSet());
934                 toKeep.addAll(toKeep.stream()
935                         .filter(s -> s.name.startsWith(BEFORE))
936                         .map(s -> steps.get(AFTER + s.name.substring(BEFORE.length())))
937                         .toList());
938                 steps.values().stream().filter(n -> !toKeep.contains(n)).forEach(BuildStep::skip);
939 
940                 plan.addProject(project, steps);
941             }
942 
943             // Create inter project dependencies
944             plan.allSteps().filter(step -> step.phase != null).forEach(step -> {
945                 Lifecycle.Phase phase = step.phase;
946                 MavenProject project = step.project;
947                 phase.links().stream().forEach(link -> {
948                     BuildStep before = plan.requiredStep(project, BEFORE + phase.name());
949                     BuildStep after = plan.requiredStep(project, AFTER + phase.name());
950                     Lifecycle.Pointer pointer = link.pointer();
951                     String n2 = pointer.phase();
952                     if (pointer instanceof Lifecycle.DependenciesPointer) {
953                         // For dependencies: ensure current project's phase starts after dependency's phase completes
954                         // Example: project's compile starts after dependency's package completes
955                         // TODO: String scope = ((Lifecycle.DependenciesPointer) pointer).scope();
956                         projects.get(project)
957                                 .forEach(p -> plan.step(p, AFTER + n2).ifPresent(before::executeAfter));
958                     } else if (pointer instanceof Lifecycle.ChildrenPointer) {
959                         // For children: ensure bidirectional phase coordination
960                         project.getCollectedProjects().forEach(p -> {
961                             // 1. Child's phase start waits for parent's phase start
962                             plan.step(p, BEFORE + n2).ifPresent(before::executeBefore);
963                             // 2. Parent's phase completion waits for child's phase completion
964                             plan.step(p, AFTER + n2).ifPresent(after::executeAfter);
965                         });
966                     }
967                 });
968             });
969 
970             // Keep projects in reactors by GAV
971             Map<String, MavenProject> reactorGavs =
972                     projects.keySet().stream().collect(Collectors.toMap(BuildPlanExecutor::gav, p -> p));
973 
974             // Go through all plugins
975             List<Runnable> toResolve = new ArrayList<>();
976             projects.keySet().forEach(project -> project.getBuild().getPlugins().forEach(plugin -> {
977                 MavenProject pluginProject = reactorGavs.get(gav(plugin));
978                 if (pluginProject != null) {
979                     // In order to plan the project, we need all its plugins...
980                     plan.requiredStep(project, PLAN).executeAfter(plan.requiredStep(pluginProject, READY));
981                 } else {
982                     toResolve.add(() -> resolvePlugin(session, project.getRemotePluginRepositories(), plugin));
983                 }
984             }));
985 
986             // Eagerly resolve all plugins in parallel
987             toResolve.parallelStream().forEach(Runnable::run);
988 
989             // Keep track of phase aliases
990             lifecycle.aliases().forEach(alias -> plan.aliases().put(alias.v3Phase(), alias.v4Phase()));
991 
992             return plan;
993         }
994     }
995 
996     private void resolvePlugin(MavenSession session, List<RemoteRepository> repositories, Plugin plugin) {
997         try {
998             mavenPluginManager.getPluginDescriptor(plugin, repositories, session.getRepositorySession());
999         } catch (Exception e) {
1000             throw new MavenException(e);
1001         }
1002     }
1003 
1004     private static String gav(MavenProject p) {
1005         return p.getGroupId() + ":" + p.getArtifactId() + ":" + p.getVersion();
1006     }
1007 
1008     private static String gav(Plugin p) {
1009         return p.getGroupId() + ":" + p.getArtifactId() + ":" + p.getVersion();
1010     }
1011 
1012     /**
1013      * Post-processes the effective configuration for the specified mojo execution. This step discards all parameters
1014      * from the configuration that are not applicable to the mojo and injects the default values for any missing
1015      * parameters.
1016      *
1017      * @param mojoExecution The mojo execution whose configuration should be finalized, must not be {@code null}.
1018      */
1019     private void finalizeMojoConfiguration(MojoExecution mojoExecution) {
1020         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
1021 
1022         XmlNode executionConfiguration = mojoExecution.getConfiguration() != null
1023                 ? mojoExecution.getConfiguration().getDom()
1024                 : null;
1025         if (executionConfiguration == null) {
1026             executionConfiguration = XmlNode.newInstance("configuration");
1027         }
1028 
1029         XmlNode defaultConfiguration = getMojoConfiguration(mojoDescriptor);
1030 
1031         List<XmlNode> children = new ArrayList<>();
1032         if (mojoDescriptor.getParameters() != null) {
1033             for (Parameter parameter : mojoDescriptor.getParameters()) {
1034                 XmlNode parameterConfiguration = executionConfiguration.child(parameter.getName());
1035 
1036                 if (parameterConfiguration == null) {
1037                     parameterConfiguration = executionConfiguration.child(parameter.getAlias());
1038                 }
1039 
1040                 XmlNode parameterDefaults = defaultConfiguration.child(parameter.getName());
1041 
1042                 if (parameterConfiguration != null) {
1043                     parameterConfiguration = XmlService.merge(parameterConfiguration, parameterDefaults, Boolean.TRUE);
1044                 } else {
1045                     parameterConfiguration = parameterDefaults;
1046                 }
1047 
1048                 if (parameterConfiguration != null) {
1049                     Map<String, String> attributes = new HashMap<>(parameterConfiguration.attributes());
1050 
1051                     String attributeForImplementation = parameterConfiguration.attribute("implementation");
1052                     String parameterForImplementation = parameter.getImplementation();
1053                     if ((attributeForImplementation == null || attributeForImplementation.isEmpty())
1054                             && ((parameterForImplementation != null) && !parameterForImplementation.isEmpty())) {
1055                         attributes.put("implementation", parameter.getImplementation());
1056                     }
1057 
1058                     parameterConfiguration = XmlNode.newInstance(
1059                             parameter.getName(),
1060                             parameterConfiguration.value(),
1061                             attributes,
1062                             parameterConfiguration.children(),
1063                             parameterConfiguration.inputLocation());
1064 
1065                     children.add(parameterConfiguration);
1066                 }
1067             }
1068         }
1069         XmlNode finalConfiguration = XmlNode.newInstance("configuration", children);
1070 
1071         mojoExecution.setConfiguration(finalConfiguration);
1072     }
1073 
1074     private XmlNode getMojoConfiguration(MojoDescriptor mojoDescriptor) {
1075         if (mojoDescriptor.isV4Api()) {
1076             return MojoDescriptorCreator.convert(mojoDescriptor.getMojoDescriptorV4());
1077         } else {
1078             return MojoDescriptorCreator.convert(mojoDescriptor).getDom();
1079         }
1080     }
1081 
1082     private MojoExecutionConfigurator mojoExecutionConfigurator(MojoExecution mojoExecution) {
1083         String configuratorId = mojoExecution.getMojoDescriptor().getComponentConfigurator();
1084         if (configuratorId == null) {
1085             configuratorId = "default";
1086         }
1087 
1088         MojoExecutionConfigurator mojoExecutionConfigurator = mojoExecutionConfigurators.get(configuratorId);
1089 
1090         if (mojoExecutionConfigurator == null) {
1091             //
1092             // The plugin has a custom component configurator but does not have a custom mojo execution configurator
1093             // so fall back to the default mojo execution configurator.
1094             //
1095             mojoExecutionConfigurator = mojoExecutionConfigurators.get("default");
1096         }
1097         return mojoExecutionConfigurator;
1098     }
1099 
1100     public static void attachToThread(MavenProject currentProject) {
1101         ClassRealm projectRealm = currentProject.getClassRealm();
1102         if (projectRealm != null) {
1103             Thread.currentThread().setContextClassLoader(projectRealm);
1104         }
1105     }
1106 
1107     protected static class Clock {
1108         Instant start;
1109         Instant end;
1110         Instant resumed;
1111         Duration exec = Duration.ZERO;
1112 
1113         protected void start() {
1114             if (start == null) {
1115                 start = MonotonicClock.now();
1116                 resumed = start;
1117             } else {
1118                 resumed = MonotonicClock.now();
1119             }
1120         }
1121 
1122         protected void stop() {
1123             end = MonotonicClock.now();
1124             exec = exec.plus(Duration.between(resumed, end));
1125         }
1126 
1127         protected Duration wallTime() {
1128             return start != null && end != null ? Duration.between(start, end) : Duration.ZERO;
1129         }
1130 
1131         protected Duration execTime() {
1132             return exec;
1133         }
1134     }
1135 }