Skip to content

Commit 6b9f298

Browse files
committed
add multi-project & multi-version support for CachedIsolation
1 parent bc726d4 commit 6b9f298

16 files changed

Lines changed: 271 additions & 37 deletions

se-commons-gradle/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies {
1818

1919
publishing {
2020
publications {
21-
maven(MavenPublication) {
21+
mavenPlugin(MavenPublication) {
2222
from(components.java)
2323
}
2424
}

se-commons-gradle/src/main/java/de/monticore/gradle/internal/ProgressLoggerService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
/**
1111
* We use a build service to pass a ProgressLogger instance to the workers
1212
* See https://github.com/gradle/gradle/issues/2678
13+
* Note: Build services cannot be serialized when isolation a workqueue (when isolating the queue)
1314
*/
1415
public abstract class ProgressLoggerService implements BuildService<BuildServiceParameters.None> {
1516

se-commons-gradle/src/main/java/de/monticore/gradle/internal/isolation/CachedIsolation.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,10 @@ protected synchronized void cleanupOld(long pCloseThreshold) {
352352
// Close closeable classloaders
353353
try {
354354
((Closeable) data.getClassLoader()).close();
355-
} catch (IOException ignored) { }
355+
} catch (Throwable ignored) {
356+
// In case java is eagerly unloading the classloader already,
357+
// used classes from libraries might result in NoClassDefErrors
358+
}
356359
}
357360
data.cleanUp();
358361
isolated.remove();

se-commons-gradle/src/main/java/de/monticore/gradle/internal/isolation/IsolatedURLClassLoader.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,14 @@ public void close() throws IOException {
7575
}
7676
}
7777
// And finally, make *really* sure we unset their context classloader
78-
for (Thread thread : Iterables.concat(Thread.getAllStackTraces().keySet(), shutdownHooks)) {
78+
for (Thread thread : Thread.getAllStackTraces().keySet()) {
79+
if (thread.getContextClassLoader() != IsolatedURLClassLoader.this)
80+
continue; // but only threads within this context
81+
thread.setContextClassLoader(null);
82+
}
83+
// duplicate code to avoid library usage here during cleanup
84+
// (in case Iterables were to be used the first time)
85+
for (Thread thread : shutdownHooks) {
7986
if (thread.getContextClassLoader() != IsolatedURLClassLoader.this)
8087
continue; // but only threads within this context
8188
thread.setContextClassLoader(null);

se-commons-gradle/src/main/java/de/monticore/gradle/queue/CachedIsolationStats.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,26 @@
55
import com.google.gson.Gson;
66

77
import javax.annotation.Nullable;
8+
import java.io.File;
9+
import java.nio.file.Files;
810
import java.util.*;
911
import java.util.stream.Collectors;
1012

1113
/**
1214
* Statistics collector for the cached isolated worker.
13-
*
15+
* Can be exported via the reportCachedQueueService gradle task
16+
* or -Dde.se_rwth.workqueue.report.continuous=true
1417
*/
1518
public class CachedIsolationStats {
16-
19+
// Unique ID for tracing reasons
20+
final UUID instanceUUID = UUID.randomUUID();
21+
1722
protected List<Event> events = Collections.synchronizedList(new ArrayList<>());
23+
protected boolean continuousTrack = shouldContinuousTrack();
24+
25+
protected boolean shouldContinuousTrack() {
26+
return "true".equals(System.getProperty("de.se_rwth.workqueue.report.continuous", "false"));
27+
}
1828

1929
void track(EventKind kind, @Nullable UUID uuid, int semaphoreMax, List<CachedQueueService.IIsolationData> runners) {
2030
track(kind, uuid, semaphoreMax, runners, null);
@@ -29,6 +39,22 @@ void track(EventKind kind, @Nullable UUID uuid, int semaphoreMax, List<CachedQue
2939
event.existingRunnerList = createRunnerList(runners);
3040
event.reason = reason;
3141
events.add(event);
42+
43+
if (continuousTrack) {
44+
report();
45+
}
46+
}
47+
48+
/**
49+
* Continuously report the current workqueue state
50+
*/
51+
protected synchronized void report() {
52+
File f = new File("report-workqueue-" + instanceUUID + ".json");
53+
try{
54+
Files.writeString(f.getAbsoluteFile().toPath(), asJson(new Gson()));
55+
}catch (Exception ignored) {
56+
57+
}
3258
}
3359

3460
public String asJson(Gson gson) {

se-commons-gradle/src/main/java/de/monticore/gradle/queue/CachedQueueService.java

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.gradle.internal.isolation.IsolatableFactory;
2525
import org.gradle.internal.operations.*;
2626
import org.gradle.internal.reflect.Instantiator;
27+
import org.gradle.internal.serialize.Decoder;
2728
import org.gradle.internal.serialize.InputStreamBackedDecoder;
2829
import org.gradle.internal.serialize.OutputStreamBackedEncoder;
2930
import org.gradle.internal.serialize.Serializer;
@@ -34,7 +35,6 @@
3435
import org.gradle.workers.WorkQueue;
3536
import org.gradle.workers.WorkerExecutor;
3637
import org.gradle.workers.internal.ActionExecutionSpecFactory;
37-
import org.gradle.workers.internal.IsolatableSerializerRegistry;
3838

3939
import javax.annotation.Nullable;
4040
import java.io.ByteArrayInputStream;
@@ -55,7 +55,7 @@
5555
* Service managing the cached isolators and their backing queue
5656
*/
5757
public abstract class CachedQueueService
58-
implements BuildService<BuildServiceParameters.None>, AutoCloseable, BuildOperationListener {
58+
implements ICachedQueueService, BuildService<BuildServiceParameters.None>, AutoCloseable, BuildOperationListener {
5959

6060
static CachedQueueService INSTANCE;
6161

@@ -299,14 +299,14 @@ void doExecuteWorkAction(ActualTaskInfo<?> info) {
299299

300300
Isolatable<WorkParameters> paramIsol = serviceRegistry.get(IsolatableFactory.class).isolate(parametersUnsafe);
301301

302-
IsolatableSerializerRegistry isolatableSerializerRegistry = this.serviceRegistry.get(IsolatableSerializerRegistry.class);
302+
IsolatableSerializerRegistryWrapper isolatableSerializerRegistry = getIsolatableSerializerRegistryWrapper();
303303

304304
ByteArrayOutputStream bos = new ByteArrayOutputStream();
305305
Serializer serializer = isolatableSerializerRegistry.build(paramIsol.getClass());
306306
try {
307307
serializer.write(new OutputStreamBackedEncoder(bos), paramIsol);
308308
} catch (Exception e) {
309-
passThrowableAlong(e);
309+
throw new RuntimeException("Failed to serialize " + info.workActionClass.getName() + " " + parameterTypeNotIsolated.getName() + " with " + parametersUnsafe.getClass(), e);
310310
}
311311

312312
String prefix = parametersUnsafe instanceof CachedIsolatedWorkQueue.WorkQueueParameters ? ((CachedIsolatedWorkQueue.WorkQueueParameters) parametersUnsafe)
@@ -635,17 +635,71 @@ public void markForErasure() {
635635

636636

637637
/**
638-
* Construc
638+
* Construct a new WorkQueue
639639
*
640640
* @param workerExecutor the worker executor to use
641641
* @param extraClasspathElement the classpath elements to use
642642
* @return a new {@link WorkQueue}
643643
*/
644644
public WorkQueue newWorkQueue(WorkerExecutor workerExecutor, FileCollection extraClasspathElement) {
645+
Objects.requireNonNull(workerExecutor, "worker executor must not be null");
646+
Objects.requireNonNull(serviceRegistry, "serviceRegistry must not be null");
645647
return new CachedIsolatedWorkQueue(workerExecutor.noIsolation(),
646648
serviceRegistry.get(InstantiatorFactory.class),
647649
Objects.requireNonNull(serviceRegistry, "serviceRegistry"),
648650
this.providerSelf,
649651
extraClasspathElement);
650652
}
653+
654+
// Gradle Version compat
655+
protected IsolatableSerializerRegistryWrapper getIsolatableSerializerRegistryWrapper() {
656+
return new IsolatableSerializerRegistryWrapper(this.serviceRegistry.get(getIsolatableSerializerRegistryClass()));
657+
}
658+
659+
protected Class<?> getIsolatableSerializerRegistryClass() {
660+
try {
661+
return Class.forName("org.gradle.workers.internal.IsolatableSerializerRegistry");
662+
}
663+
catch (ClassNotFoundException e) {
664+
try {
665+
return Class.forName("org.gradle.internal.snapshot.impl.IsolatableSerializerRegistry");
666+
}
667+
catch (ClassNotFoundException ex) {
668+
throw new IllegalStateException(ex);
669+
}
670+
}
671+
}
672+
673+
/**
674+
* Wrapper around gradle internals.
675+
* Must not refer to gradle internal classes to avoid errors during plugin-ASM-phase
676+
*/
677+
protected static class IsolatableSerializerRegistryWrapper {
678+
679+
final Object instance;
680+
681+
IsolatableSerializerRegistryWrapper(Object instance) {
682+
this.instance = instance;
683+
}
684+
685+
<T> Serializer<T> build(Class<T> baseType) {
686+
try {
687+
return (Serializer<T>) instance.getClass().getMethod("build", Class.class)
688+
.invoke(instance, baseType);
689+
}
690+
catch (ReflectiveOperationException e) {
691+
throw new IllegalStateException("Failed to wrap IsolatableSerializerRegistry", e);
692+
}
693+
}
694+
695+
Isolatable<?> readIsolatable(Decoder decoder) {
696+
try {
697+
return (Isolatable<?>) instance.getClass().getMethod("readIsolatable", Decoder.class)
698+
.invoke(instance, decoder);
699+
}
700+
catch (ReflectiveOperationException e) {
701+
throw new IllegalStateException("Failed to wrap IsolatableSerializerRegistry", e);
702+
}
703+
}
704+
}
651705
}

se-commons-gradle/src/main/java/de/monticore/gradle/queue/CachedQueueServicePlugin.java

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import org.checkerframework.checker.nullness.qual.NonNull;
55
import org.gradle.api.Plugin;
66
import org.gradle.api.Project;
7-
import org.gradle.api.provider.Provider;
87
import org.gradle.api.tasks.TaskProvider;
98

109
/**
@@ -13,29 +12,17 @@
1312
public class CachedQueueServicePlugin implements Plugin<Project> {
1413
@Override
1514
public void apply(@NonNull Project project) {
16-
// Register the service, if needed
17-
Provider<CachedQueueService> sp = project.getGradle().getSharedServices().registerIfAbsent(CachedQueueService.NAME, CachedQueueService.class, spec -> {
18-
});
15+
// Register the service in the settings plugin
16+
project.getGradle().getPluginManager().apply(InternalCachedQueueSetupSettingsPlugin.class);
1917
if (project == project.getRootProject()) {
20-
// initialize the service but only if it is the root project
21-
sp.get().init(project.getGradle());
22-
2318
// Register an optional reporting task
2419
TaskProvider<ReportCachedQueueServiceTask> reportTask = project.getTasks().register("reportCachedQueueService", ReportCachedQueueServiceTask.class, spec -> {
2520
spec.mustRunAfter(project.getTasks().withType(ICachedQueueTask.class));
26-
spec.getSharedQueueService().set(sp);
2721
});
2822
project.getTasks().withType(ICachedQueueTask.class).configureEach(task -> {
2923
task.finalizedBy(reportTask);
3024
});
3125
}
32-
33-
34-
// And configure all tasks with the ICachedQueueTask interface to access this service
35-
36-
project.getTasks().withType(ICachedQueueTask.class).configureEach(task -> {
37-
task.getSharedQueueService().set(sp);
38-
});
3926
}
4027

4128
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package de.monticore.gradle.queue;
2+
3+
import org.gradle.api.file.FileCollection;
4+
import org.gradle.workers.WorkQueue;
5+
import org.gradle.workers.WorkerExecutor;
6+
7+
/**
8+
* @since 7.9.0
9+
*/
10+
public interface ICachedQueueService {
11+
12+
/**
13+
* Construct a new WorkQueue
14+
*
15+
* @param workerExecutor the worker executor to use
16+
* @param extraClasspathElement the classpath elements to use
17+
* @return a new {@link WorkQueue}
18+
*/
19+
WorkQueue newWorkQueue(WorkerExecutor workerExecutor, FileCollection extraClasspathElement);
20+
21+
/**
22+
* Returns the tracked stats as a serialized JSON string.
23+
* @see CachedIsolationStats
24+
* @return a serialized JSON string of the stats
25+
*/
26+
String getStats();
27+
}

se-commons-gradle/src/main/java/de/monticore/gradle/queue/ICachedQueueTask.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,39 @@
55
import org.gradle.api.provider.Property;
66
import org.gradle.api.tasks.Internal;
77

8+
import java.lang.reflect.Proxy;
9+
810
/**
911
* A task with access to a cached queue service.
1012
* Automatically managed via the {@link CachedQueueServicePlugin}
1113
*/
1214
public interface ICachedQueueTask extends Task {
15+
// Type must be object due to https://github.com/gradle/gradle/issues/17559
16+
@Internal
17+
Property<Object> getSharedQueueServiceProperty();
18+
19+
@Internal
20+
@Deprecated(forRemoval = true)
21+
default Property<ICachedQueueService> getSharedQueueService() {
22+
return (Property<ICachedQueueService>)( (Property)getSharedQueueServiceProperty());
23+
}
24+
1325
@Internal
14-
Property<CachedQueueService> getSharedQueueService();
26+
default ICachedQueueService doGetSharedQueueService() {
27+
Object sharedQueueService = getSharedQueueServiceProperty().get();
28+
if (sharedQueueService instanceof ICachedQueueService) {
29+
return (ICachedQueueService) getSharedQueueServiceProperty().get();
30+
}
31+
System.err.println("Invoking CachedQueueService cast workaround");
32+
// Gradle might load the plugin into multiple classloaders (once per subproject where it is applied)
33+
// If that is the case, ICachedQueueService (from CL 1) is not castable to ICachedQueueService (from CL 2)
34+
// As we only expose the ICachedQueueService, we can create a proxy between both classloaders
35+
return (ICachedQueueService) Proxy.newProxyInstance(CachedQueueService.class.getClassLoader(),
36+
new Class[] { ICachedQueueService.class }, (proxy, method, args) -> {
37+
System.err.println("called " + method.getName());
38+
Object realObject = getSharedQueueServiceProperty().get();
39+
return realObject.getClass().getMethod(method.getName(), method.getParameterTypes()).invoke(realObject, args);
40+
});
41+
}
42+
1543
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/* (c) https://github.com/MontiCore/monticore */
2+
package de.monticore.gradle.queue;
3+
4+
import org.checkerframework.checker.nullness.qual.NonNull;
5+
import org.gradle.api.Plugin;
6+
import org.gradle.api.Project;
7+
import org.gradle.api.invocation.Gradle;
8+
import org.gradle.api.logging.Logger;
9+
import org.gradle.api.logging.Logging;
10+
import org.gradle.api.provider.Provider;
11+
12+
/**
13+
* Base plugin managing the cached isolation service
14+
*/
15+
public class InternalCachedQueueSetupSettingsPlugin implements Plugin<Gradle> {
16+
protected Logger logger = Logging.getLogger(CachedQueueService.class);
17+
@Override
18+
public void apply(@NonNull Gradle gradle) {
19+
// Register the service, if needed
20+
Provider<CachedQueueService> sp = gradle.getSharedServices().registerIfAbsent(CachedQueueService.NAME, CachedQueueService.class, spec -> {
21+
});
22+
try {
23+
sp.get().init(gradle);
24+
}catch (ClassCastException ignored) {
25+
logger.warn("WARNING: The se-commons cached work queue is setup multiple times.\n"
26+
+ "Reporting, log prefixes, and performance might be incorrect. \n"
27+
+ "Please add the (possibly MontiCore Generator) plugin to your root plugin (possible with apply false). \n"
28+
+ "More details: https://monticore.github.io/monticore/docs/Gradle/#root-warning");
29+
}
30+
gradle.allprojects(p->withType(p, sp));
31+
}
32+
33+
protected void withType(Project p, Provider<CachedQueueService> sp) {
34+
p.getTasks().withType(ICachedQueueTask.class).configureEach(task -> {
35+
task.getSharedQueueServiceProperty().set(sp);
36+
});
37+
// The report task has to avoid circular dependencies
38+
p.getTasks().withType(ReportCachedQueueServiceTask.class).configureEach(task -> {
39+
task.getSharedQueueService().set(sp);
40+
});
41+
}
42+
43+
}

0 commit comments

Comments
 (0)