Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that no-isolation workers run with the classloader of the submitting thread #10335

Merged
merged 4 commits into from
Aug 22, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/*
* Copyright 2019 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 org.gradle.integtests.fixtures.AbstractIntegrationSpec
import org.gradle.workers.fixtures.WorkerExecutorFixture
import spock.lang.Unroll

class WorkerExecutorCompositeBuildIntegrationTest extends AbstractIntegrationSpec {
WorkerExecutorFixture fixture = new WorkerExecutorFixture(temporaryFolder)
def plugin = testDirectory.createDir("plugin")
def build1 = testDirectory.createDir("build1")

@Unroll
def "can use worker api with composite builds using #pluginId"() {
settingsFile << """
includeBuild "plugin"
includeBuild "build1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you use a better name for build1? this looks like it's some kind of empty dependency for the root project. Maybe just "dependency"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough - I've changed the project names to "app" and "lib".

"""

withFileHelperInPluginBuild()
withLegacyRunnableAndTaskInPluginBuild()
withTypedWorkerPluginInPluginBuild()

build1.file("build.gradle") << """
buildscript {
repositories {
${jcenterRepository()}
}

dependencies {
classpath "org.apache.commons:commons-math:2.2"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary to trigger the breakage? If so, could you put a comment explaining how this is important?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is necessary since classloader caching would cause an identical classpath to be returned for both builds unless there is some difference. However, it's not necessary that it actually be a real dependency. I've changed this to just an un-managed file and eliminated the external dependency.

}
}

plugins {
id "java"
id "org.gradle.test.${pluginId}"
}

group = "org.gradle.test"
version = "1.0"

jar {
from runWork
}
"""

buildFile << """
plugins {
id "java"
id "org.gradle.test.${pluginId}"
}

dependencies {
compile "org.gradle.test:build1:1.0"
}

runWork.dependsOn compileJava
"""

expect:
succeeds("runWork")

where:
pluginId << [ 'legacy-worker-plugin', 'typed-worker-plugin' ]
}

private void withLegacyRunnableAndTaskInPluginBuild() {
plugin.file("src/main/java/LegacyParameter.java") << """
import java.io.File;
import java.io.Serializable;

public class LegacyParameter implements Serializable {
private final File outputFile;

public LegacyParameter(File outputFile) {
this.outputFile = outputFile;
}

public File getOutputFile() {
return this.outputFile;
}
}
"""

plugin.file('src/main/java/LegacyRunnable.java') << """
import javax.inject.Inject;
import java.io.File;
import org.gradle.test.FileHelper;

public class LegacyRunnable implements Runnable {
private File outputFile;

@Inject
public LegacyRunnable(LegacyParameter parameter) {
this.outputFile = parameter.getOutputFile();
}

public void run() {
FileHelper.write("foo", outputFile);
}
}
"""

plugin.file('src/main/java/LegacyWorkerTask.java') << """
import org.gradle.api.DefaultTask;
import org.gradle.api.Action;
import org.gradle.api.tasks.*;
import org.gradle.workers.*;
import javax.inject.Inject;
import java.io.File;

public class LegacyWorkerTask extends DefaultTask {
private final WorkerExecutor workerExecutor;
private File outputFile;

@Inject
public LegacyWorkerTask(WorkerExecutor workerExecutor) {
this.workerExecutor = workerExecutor;
}

@TaskAction
public void runWork() {
workerExecutor.submit(LegacyRunnable.class, new Action<WorkerConfiguration>() {
public void execute(WorkerConfiguration config) {
config.setIsolationMode(IsolationMode.NONE);
config.params(new LegacyParameter(outputFile));
}
});
}

@OutputFile
File getOutputFile() {
return outputFile;
}

void setOutputFile(File outputFile) {
this.outputFile = outputFile;
}
}
"""

plugin.file("src/main/java/LegacyWorkerPlugin.java") << """
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import java.io.File;

public class LegacyWorkerPlugin implements Plugin<Project> {
public void apply(Project project) {
project.getTasks().create("runWork", LegacyWorkerTask.class)
.setOutputFile(new File(project.getBuildDir(), "workOutput"));
}
}
"""

plugin.file("build.gradle") << """
apply plugin: "java-gradle-plugin"

gradlePlugin {
plugins {
LegacyWorkerPlugin {
id = "org.gradle.test.legacy-worker-plugin"
implementationClass = "LegacyWorkerPlugin"
}
}
}
"""
}

private void withTypedWorkerPluginInPluginBuild() {
plugin.file("src/main/java/TypedParameter.java") << """
import org.gradle.workers.WorkParameters;
import org.gradle.api.file.RegularFileProperty;

public interface TypedParameter extends WorkParameters {
RegularFileProperty getOutputFile();
}
"""

plugin.file("src/main/java/TypedWorkAction.java") << """
import org.gradle.workers.WorkAction;
import org.gradle.test.FileHelper;

abstract public class TypedWorkAction implements WorkAction<TypedParameter> {
public void execute() {
FileHelper.write("foo", getParameters().getOutputFile().getAsFile().get());
}
}
"""

plugin.file("src/main/java/TypedWorkerTask.java") << """
import org.gradle.api.DefaultTask;
import org.gradle.api.Action;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.*;
import org.gradle.workers.*;
import javax.inject.Inject;
import java.io.File;

public class TypedWorkerTask extends DefaultTask {
private final WorkerExecutor workerExecutor;
private RegularFileProperty outputFile;

@Inject
public TypedWorkerTask(WorkerExecutor workerExecutor) {
this.workerExecutor = workerExecutor;
this.outputFile = getProject().getObjects().fileProperty();
}

@TaskAction
public void runWork() {
workerExecutor.noIsolation().submit(TypedWorkAction.class, new Action<TypedParameter>() {
public void execute(TypedParameter parameters) {
parameters.getOutputFile().set(outputFile);
}
});
}

@OutputFile
RegularFileProperty getOutputFile() {
return outputFile;
}
}
"""

plugin.file("src/main/java/TypedWorkerPlugin.java") << """
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import java.io.File;

public class TypedWorkerPlugin implements Plugin<Project> {
public void apply(Project project) {
project.getTasks().create("runWork", TypedWorkerTask.class)
.getOutputFile().set(new File(project.getBuildDir(), "workOutput"));
}
}
"""

plugin.file("build.gradle") << """
apply plugin: "java-gradle-plugin"

gradlePlugin {
plugins {
TypedWorkerPlugin {
id = "org.gradle.test.typed-worker-plugin"
implementationClass = "TypedWorkerPlugin"
}
}
}
"""
}

private void withFileHelperInPluginBuild() {
plugin.file("src/main/java/org/gradle/test/FileHelper.java") << fixture.fileHelperClass
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.gradle.api.specs.Spec;
import org.gradle.internal.Actions;
import org.gradle.internal.Cast;
import org.gradle.internal.Factory;
import org.gradle.internal.classloader.ClassLoaderUtils;
import org.gradle.internal.exceptions.Contextual;
import org.gradle.internal.exceptions.DefaultMultiCauseException;
import org.gradle.internal.operations.BuildOperationExecutor;
Expand Down Expand Up @@ -52,6 +54,7 @@
import org.gradle.workers.WorkParameters;
import org.gradle.workers.WorkerSpec;

import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import java.io.File;
import java.lang.reflect.ParameterizedType;
Expand Down Expand Up @@ -203,11 +206,11 @@ private <T extends WorkParameters> AsyncWorkCompletion submitWork(Class<? extend
private AsyncWorkCompletion submitWork(final ActionExecutionSpec spec, final IsolationMode isolationMode, final DaemonForkOptions daemonForkOptions) {
final WorkerLease currentWorkerWorkerLease = getCurrentWorkerLease();
final BuildOperationRef currentBuildOperation = buildOperationExecutor.getCurrentOperation();
WorkerFactory workerFactory = getWorkerFactory(isolationMode);
WorkItemExecution execution = new WorkItemExecution(spec.getDisplayName(), currentWorkerWorkerLease, new Callable<DefaultWorkResult>() {
@Override
public DefaultWorkResult call() throws Exception {
try {
WorkerFactory workerFactory = getWorkerFactory(isolationMode);
BuildOperationAwareWorker worker = workerFactory.getWorker(daemonForkOptions);
return worker.execute(spec, currentBuildOperation);
} catch (Throwable t) {
Expand Down Expand Up @@ -247,7 +250,7 @@ private WorkerFactory getWorkerFactory(IsolationMode isolationMode) {
case CLASSLOADER:
return isolatedClassloaderWorkerFactory;
case NONE:
return noIsolationWorkerFactory;
return new ContextClassLoaderWorkerFactory(noIsolationWorkerFactory);
case PROCESS:
return daemonWorkerFactory;
default:
Expand Down Expand Up @@ -442,4 +445,35 @@ public void await() throws WorkerExecutionException {
workerExecutor.await(workItems);
}
}

/**
* This is a delegating WorkerFactory that captures the context classloader at the moment it is created
* and then ensures that the worker is created with the same context classloader when
* getWorker() is called later.
*/
private static class ContextClassLoaderWorkerFactory implements WorkerFactory {
private final ClassLoader contextClassLoader;
private final WorkerFactory delegate;

public ContextClassLoaderWorkerFactory(WorkerFactory delegate) {
this.delegate = delegate;
this.contextClassLoader = Thread.currentThread().getContextClassLoader();
}

@Override
public BuildOperationAwareWorker getWorker(DaemonForkOptions forkOptions) {
return ClassLoaderUtils.executeInClassloader(contextClassLoader, new Factory<BuildOperationAwareWorker>() {
@Nullable
@Override
public BuildOperationAwareWorker create() {
return delegate.getWorker(forkOptions);
}
});
}

@Override
public IsolationMode getIsolationMode() {
return delegate.getIsolationMode();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ class WorkerExecutorFixture {
}

def prepareTaskTypeUsingWorker() {
withParameterClassInBuildSrc()
withFileHelperClassInBuildSrc()

buildFile << """
import org.gradle.workers.*
$taskTypeUsingWorker
"""
}

String getTaskTypeUsingWorker() {
withParameterClassInBuildSrc()
withFileHelperClassInBuildSrc()

return """
import javax.inject.Inject
import org.gradle.other.Foo
Expand Down Expand Up @@ -241,7 +241,7 @@ class WorkerExecutorFixture {
import java.io.FileWriter;

public class FileHelper {
static void write(String id, File outputFile) {
public static void write(String id, File outputFile) {
PrintWriter out = null;
try {
outputFile.getParentFile().mkdirs();
Expand Down
1 change: 1 addition & 0 deletions subprojects/workers/workers.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
integTestRuntimeOnly(project(":kotlinDsl"))
integTestRuntimeOnly(project(":kotlinDslProviderPlugins"))
integTestRuntimeOnly(project(":apiMetadata"))
integTestRuntimeOnly(project(":testKit"))

integTestImplementation(project(":jvmServices"))
integTestImplementation(project(":internalIntegTesting"))
Expand Down