/* * Copyright 2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.gradle.workers.internal; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import org.gradle.api.Action; import org.gradle.api.Transformer; import org.gradle.api.internal.file.FileResolver; import org.gradle.internal.UncheckedException; import org.gradle.internal.classloader.ClasspathUtil; import org.gradle.internal.classloader.FilteringClassLoader; import org.gradle.internal.concurrent.ExecutorFactory; import org.gradle.internal.exceptions.Contextual; import org.gradle.internal.exceptions.DefaultMultiCauseException; import org.gradle.internal.operations.BuildOperationExecutor; import org.gradle.internal.progress.BuildOperationState; import org.gradle.internal.work.AsyncWorkCompletion; import org.gradle.internal.work.AsyncWorkTracker; import org.gradle.internal.work.WorkerLeaseRegistry; import org.gradle.internal.work.WorkerLeaseRegistry.WorkerLease; import org.gradle.process.JavaForkOptions; import org.gradle.util.CollectionUtils; import org.gradle.workers.IsolationMode; import org.gradle.workers.WorkerConfiguration; import org.gradle.workers.WorkerExecutionException; import org.gradle.workers.WorkerExecutor; import java.io.File; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; public class DefaultWorkerExecutor implements WorkerExecutor { private final ListeningExecutorService executor; private final WorkerFactory daemonWorkerFactory; private final WorkerFactory isolatedClassloaderWorkerFactory; private final WorkerFactory noIsolationWorkerFactory; private final FileResolver fileResolver; private final WorkerLeaseRegistry workerLeaseRegistry; private final BuildOperationExecutor buildOperationExecutor; private final AsyncWorkTracker asyncWorkTracker; public DefaultWorkerExecutor(WorkerFactory daemonWorkerFactory, WorkerFactory isolatedClassloaderWorkerFactory, WorkerFactory noIsolationWorkerFactory, FileResolver fileResolver, ExecutorFactory executorFactory, WorkerLeaseRegistry workerLeaseRegistry, BuildOperationExecutor buildOperationExecutor, AsyncWorkTracker asyncWorkTracker) { this.daemonWorkerFactory = daemonWorkerFactory; this.isolatedClassloaderWorkerFactory = isolatedClassloaderWorkerFactory; this.noIsolationWorkerFactory = noIsolationWorkerFactory; this.fileResolver = fileResolver; this.executor = MoreExecutors.listeningDecorator(executorFactory.create("Worker Daemon Execution")); this.workerLeaseRegistry = workerLeaseRegistry; this.buildOperationExecutor = buildOperationExecutor; this.asyncWorkTracker = asyncWorkTracker; } @Override public void submit(Class<? extends Runnable> actionClass, Action<? super WorkerConfiguration> configAction) { WorkerConfiguration configuration = new DefaultWorkerConfiguration(fileResolver); configAction.execute(configuration); String description = configuration.getDisplayName() != null ? configuration.getDisplayName() : actionClass.getName(); // Serialize parameters in this thread prior to starting work in a separate thread ActionExecutionSpec spec; try { spec = new ActionExecutionSpec(actionClass, description, configuration.getParams()); } catch (Throwable t) { throw new WorkExecutionException(description, t); } submit(spec, configuration.getForkOptions().getWorkingDir(), configuration.getIsolationMode(), getDaemonForkOptions(actionClass, configuration)); } private void submit(final ActionExecutionSpec spec, final File workingDir, final IsolationMode isolationMode, final DaemonForkOptions daemonForkOptions) { final WorkerLease currentWorkerWorkerLease = workerLeaseRegistry.getCurrentWorkerLease(); final BuildOperationState currentBuildOperation = buildOperationExecutor.getCurrentOperation(); ListenableFuture<DefaultWorkResult> workerDaemonResult = executor.submit(new Callable<DefaultWorkResult>() { @Override public DefaultWorkResult call() throws Exception { try { WorkerFactory workerFactory = getWorkerFactory(isolationMode); Worker<ActionExecutionSpec> worker = workerFactory.getWorker(WorkerDaemonServer.class, workingDir, daemonForkOptions); return worker.execute(spec, currentWorkerWorkerLease, currentBuildOperation); } catch (Throwable t) { throw new WorkExecutionException(spec.getDisplayName(), t); } } }); registerAsyncWork(spec.getDisplayName(), workerDaemonResult); } private WorkerFactory getWorkerFactory(IsolationMode isolationMode) { switch(isolationMode) { case AUTO: case CLASSLOADER: return isolatedClassloaderWorkerFactory; case NONE: return noIsolationWorkerFactory; case PROCESS: return daemonWorkerFactory; default: throw new IllegalArgumentException("Unknown isolation mode: " + isolationMode); } } void registerAsyncWork(final String description, final Future<DefaultWorkResult> workItem) { asyncWorkTracker.registerWork(buildOperationExecutor.getCurrentOperation(), new AsyncWorkCompletion() { @Override public void waitForCompletion() { try { DefaultWorkResult result = workItem.get(); if (!result.isSuccess()) { throw new WorkExecutionException(description, result.getException()); } } catch (InterruptedException e) { throw UncheckedException.throwAsUncheckedException(e); } catch (ExecutionException e) { throw UncheckedException.throwAsUncheckedException(e); } } @Override public boolean isComplete() { return workItem.isDone(); } }); } @Override public void await() throws WorkerExecutionException { BuildOperationState currentOperation = buildOperationExecutor.getCurrentOperation(); try { asyncWorkTracker.waitForCompletion(currentOperation); } catch (DefaultMultiCauseException e) { throw workerExecutionException(e.getCauses()); } } private WorkerExecutionException workerExecutionException(List<? extends Throwable> failures) { if (failures.size() == 1) { throw new WorkerExecutionException("There was a failure while executing work items", failures); } else { throw new WorkerExecutionException("There were multiple failures while executing work items", failures); } } DaemonForkOptions getDaemonForkOptions(Class<?> actionClass, WorkerConfiguration configuration) { validateWorkerConfiguration(configuration); Iterable<Class<?>> paramTypes = CollectionUtils.collect(configuration.getParams(), new Transformer<Class<?>, Object>() { @Override public Class<?> transform(Object o) { return o.getClass(); } }); return toDaemonOptions(actionClass, paramTypes, configuration.getForkOptions(), configuration.getClasspath()); } private void validateWorkerConfiguration(WorkerConfiguration configuration) { if (configuration.getIsolationMode() == IsolationMode.NONE) { if (configuration.getClasspath().iterator().hasNext()) { throw unsupportedWorkerConfigurationException("classpath", configuration.getIsolationMode()); } } if (configuration.getIsolationMode() == IsolationMode.NONE || configuration.getIsolationMode() == IsolationMode.CLASSLOADER) { if (!configuration.getForkOptions().getBootstrapClasspath().isEmpty()) { throw unsupportedWorkerConfigurationException("bootstrap classpath", configuration.getIsolationMode()); } if (!configuration.getForkOptions().getJvmArgs().isEmpty()) { throw unsupportedWorkerConfigurationException("jvm arguments", configuration.getIsolationMode()); } if (configuration.getForkOptions().getMaxHeapSize() != null) { throw unsupportedWorkerConfigurationException("maximum heap size", configuration.getIsolationMode()); } if (configuration.getForkOptions().getMinHeapSize() != null) { throw unsupportedWorkerConfigurationException("minimum heap size", configuration.getIsolationMode()); } if (!configuration.getForkOptions().getSystemProperties().isEmpty()) { throw unsupportedWorkerConfigurationException("system properties", configuration.getIsolationMode()); } } } private RuntimeException unsupportedWorkerConfigurationException(String propertyDescription, IsolationMode isolationMode) { return new UnsupportedOperationException("The worker " + propertyDescription + " cannot be set when using isolation mode " + isolationMode.name()); } private DaemonForkOptions toDaemonOptions(Class<?> actionClass, Iterable<Class<?>> paramClasses, JavaForkOptions forkOptions, Iterable<File> classpath) { ImmutableSet.Builder<File> classpathBuilder = ImmutableSet.builder(); ImmutableSet.Builder<String> sharedPackagesBuilder = ImmutableSet.builder(); sharedPackagesBuilder.add("javax.inject"); if (classpath != null) { classpathBuilder.addAll(classpath); } addVisibilityFor(actionClass, classpathBuilder, sharedPackagesBuilder, true); for (Class<?> paramClass : paramClasses) { addVisibilityFor(paramClass, classpathBuilder, sharedPackagesBuilder, false); } Iterable<File> daemonClasspath = classpathBuilder.build(); Iterable<String> daemonSharedPackages = sharedPackagesBuilder.build(); return new DaemonForkOptions(forkOptions.getMinHeapSize(), forkOptions.getMaxHeapSize(), forkOptions.getAllJvmArgs(), daemonClasspath, daemonSharedPackages); } private static void addVisibilityFor(Class<?> visibleClass, ImmutableSet.Builder<File> classpathBuilder, ImmutableSet.Builder<String> sharedPackagesBuilder, boolean addToSharedPackages) { if (visibleClass.getClassLoader() != null) { classpathBuilder.addAll(ClasspathUtil.getClasspath(visibleClass.getClassLoader()).getAsFiles()); } if (addToSharedPackages) { addVisiblePackage(visibleClass, sharedPackagesBuilder); } } private static void addVisiblePackage(Class<?> visibleClass, ImmutableSet.Builder<String> sharedPackagesBuilder) { if (visibleClass.getPackage() == null || "".equals(visibleClass.getPackage().getName())) { sharedPackagesBuilder.add(FilteringClassLoader.DEFAULT_PACKAGE); } else { sharedPackagesBuilder.add(visibleClass.getPackage().getName()); } } @Contextual private static class WorkExecutionException extends RuntimeException { WorkExecutionException(String description, Throwable cause) { super("A failure occurred while executing " + description, cause); } } }