Gradle configuration cache by example. Part 2: problems
In the first part we have seen how configuration cache is working. Now I will briefly show what problems configuration cache brings to plugin authors with possible solutions.
Searching for problems
There are two types of problems you may face:
On first run (with cache enabled) gradle could indicate problems with configuration state serialization . Not always obvious to track, but, ususallly, quite easy to fix.
On second run (from cache) there might be problems because of incorrect assumptions. For example, you rely on build service data (filled at configuration time), which is not preserved when running from cache (build service state not serialized).
We will see both types of problems below.
Access project at runtime
The simplest and the most popular problem is using not allowed objects at runtime. Most likely, you’ll face project usage in task:
public abstract class Fail1Task extends DefaultTask {
@TaskAction
public void run() {
System.out.println("[run] Task executed for project: " + getProject().getName());
}
}
And a very simple plugin:
public abstract class Fail1Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getTasks().register("fail1Task", Fail1Task.class);
}
}
It will fail to run fail1Task --configuration-cache --configuration-cache-problems=warn
Calculating task graph as no cached configuration is available for tasks: fail1Task
> Task :fail1Task
[run] Task executed for project: junit14814233387960710613
1 problem was found storing the configuration cache.
- Task `:fail1Task` of type `ru.vyarus.gradle.plugin.fails.fail1.Fail1Task`: invocation of 'Task.project' at execution time is unsupported.
See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution
See the complete report at file:///tmp/junit14814233387960710613/build/reports/configuration-cache/2n0qomukpcgdka6np2vpry01g/a4zpdprgh0056ayfdj5dwvtkk/configuration-cache-report.html
[Incubating] Problems report is available at: file:///tmp/junit14814233387960710613/build/reports/problems/problems-report.html
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored with 1 problem.
Note: configuration-cache-problems=warn is used to prevent build failure for simpler output (without stacktraces) and to show some additional side effects (in later cases).
To fix this, project object access must be moved to configuration phase. It could be done by storing required data in task property in the constructor (called at configuration time):
public abstract class Fail1FixTask extends DefaultTask {
private final String projectName;
public Fail1FixTask() {
projectName = getProject().getName();
}
@TaskAction
public void run() {
System.out.println("[run] Task executed for project: " + projectName);
}
}
Or, with provider (if state resolution must be delayed):
public abstract class Fail1Fix2Task extends DefaultTask {
private final Provider<String> projectName;
public Fail1Fix2Task() {
projectName = getProject().provider(() -> getProject().getName());
}
@TaskAction
public void run() {
System.out.println("[run] Task executed for project: " + projectName.get());
}
}
IMPORTANT: Cases with other gradle objects are not shown becuase they are clearly described in the docs.
Project usage in plugin
Project (or other not allowed objects) usage at plugin’s run block also fails. The plugin blow access project in the task’s doLast block (executed at runtime):
public abstract class Fail2Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
System.out.println("[configuration] Project name: " + project.getName());
project.getTasks().register("fail2Task", task -> {
task.doLast(task1 ->
System.out.println("[run] Project name: " + project.getName()));
});
}
}
Even with cache warnings enabled, task execution will failfail2Task --configuration-cache --configuration-cache-problems=warn:
Calculating task graph as no cached configuration is available for tasks: fail2Task
> Configure project :
[configuration] Project name: junit16727929338108926619
> Task :fail2Task FAILED
2 problems were found storing the configuration cache.
- Task `:fail2Task` of type `org.gradle.api.DefaultTask`: cannot deserialize object of type 'org.gradle.api.Project' as these are not supported with the configuration cache.
See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types
- Task `:fail2Task` of type `org.gradle.api.DefaultTask`: cannot serialize object of type 'org.gradle.api.internal.project.DefaultProject', a subtype of 'org.gradle.api.Project', as these are not supported with the configuration cache.
See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types
See the complete report at file:///tmp/junit16727929338108926619/build/reports/configuration-cache/47ehjfb227oo5kzbg615m5h3q/dghkqx053bip47mmv9kiov6f3/configuration-cache-report.html
[Incubating] Problems report is available at: file:///tmp/junit16727929338108926619/build/reports/problems/problems-report.html
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':fail2Task'.
> Cannot invoke "org.gradle.api.Project.getName()" because "project" is null
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':fail2Task'.
...
Caused by: java.lang.NullPointerException: Cannot invoke "org.gradle.api.Project.getName()" because "project" is null
...
BUILD FAILED in 3s
1 actionable task: 1 executed
Configuration cache entry stored with 2 problems.
It failed with NullPointerException becuase, even on first run, it serialize and deserialize state, required for runtime blocks (in the separate task case above, the project call was a part of runtime action, not configuration).
Same as with task case, the fix is to store required data in a variable or using provider in configuration phase:
@Override
public void apply(Project project) {
final String projectName = project.getName();
project.getTasks().register("fail2Fix", task -> {
task.doLast(task1 ->
System.out.println("[run] Project name: " + projectName));
});
final Provider<String> nameProvider = project.provider(() -> {
System.out.println("[configuration] Provider called");
return project.getName();
});
project.getTasks().register("fail2Fix2", task -> {
task.doLast(task1 ->
System.out.println("[run] Project name: " + nameProvider.get()));
});
}
Running fail2Fix fail2Fix2 --configuration-cache --configuration-cache-problems=warn:
Calculating task graph as no cached configuration is available for tasks: fail2Fix fail2Fix2
[configuration] Provider called
> Task :fail2Fix2
[run] Project name: junit17295962152626235093
> Task :fail2Fix
[run] Project name: junit17295962152626235093
BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed
Configuration cache entry stored.
(the order of tasks is not determinate as they run in parallel)
Runtime blocks in plugin is the main “problem” for configuration cache: it applies much efforts analyzing them to serialize (only) required configuration state.
Too broad serialization
This is the most annoying problem as gradle can’t hint you about its source. For example, here is an extension with not serializable object (SourceSet):
public class Fail3Extension {
public Set<SourceSet> sets;
public String message;
public Fail3Extension(Project project) {
this.sets = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets();
}
}
Plugin only use string extension property at runtime (and does not call not serializable property!):
public class Fail3Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
Fail3Extension ext = project.getExtensions().create("fail3", Fail3Extension.class, project);
project.getTasks().register("fail3Task", task -> {
task.doLast(task1 ->
System.out.println("[run] Message: " + ext.message));
});
}
}
But gradle would try to serialize the entire extension here (object, referenced at runtime)fail3Task --configuration-cache --configuration-cache-problems=warn:
Calculating task graph as no cached configuration is available for tasks: fail3Task
> Task :fail3Task
[run] Message: Configured!
2 problems were found storing the configuration cache.
- Task `:fail3Task` of type `org.gradle.api.DefaultTask`: cannot deserialize object of type 'org.gradle.api.tasks.SourceSetContainer' as these are not supported with the configuration cache.
See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types
- Task `:fail3Task` of type `org.gradle.api.DefaultTask`: cannot serialize object of type 'org.gradle.api.internal.tasks.DefaultSourceSetContainer', a subtype of 'org.gradle.api.tasks.SourceSetContainer', as these are not supported with the configuration cache.
See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types
See the complete report at file:///tmp/junit10544361871289601409/build/reports/configuration-cache/5tucq48qd6ozznv386suhaql7/9u8dhuyl2ja1knarlvrf25z8q/configuration-cache-report.html
[Incubating] Problems report is available at: file:///tmp/junit10544361871289601409/build/reports/problems/problems-report.html
BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed
Configuration cache entry stored with 2 problems.
You can see here that gradle only points you to a not serializable type and you must analyze all runtime blocks yourself to understand the source of problem.
To fix this particular problem, reduce caches scope by assigninig exact required data to a variable in the configuration phase:
@Override
public void apply(Project project) {
Fail3Extension ext = project.getExtensions().create("fail3", Fail3Extension.class, project);
project.getTasks().register("fail3Task", task -> {
// reduce serialization scope
String message = ext.message;
task.doLast(task1 ->
System.out.println("[run] Message: " + message));
});
}
Here variable is assigned in the task’s lazy initialization block (and so user configuration is applied). It is important to not assign variable too early as user configuration may not be applied, like here:
// it would be NULL! Too early
String message = ext.message;
project.getTasks().register("fail3Task", task -> {
task.doLast(task1 ->
System.out.println("[run] Message: " + message));
});
With provider the exact declaration place is not important:
Provider<String> message = project.provider(() -> ext.message);
project.getTasks().register("fail3Task", task -> {
task.doLast(task1 ->
System.out.println("[run] Message: " + message));
});
Local variable, cachable or non-cachable (ValueSource) provider usage is the magic wand for configuration cache compatiblity.
The difference of plugin and task serialization
Tasks and plugins are serialized differently! To show it, we will store not allowed object (SourceSet) in task and plugin fields, but will not use it at runtime.
The task:
public abstract class Fail4Task extends DefaultTask {
private SourceSet sourceSet;
private String name;
public Fail4Task() {
sourceSet = getProject().getExtensions().getByType(JavaPluginExtension.class).getSourceSets().getByName("main");
name = sourceSet.getName();
}
@TaskAction
public void run() {
System.out.println("[run] Task source set: " + name);
}
}
The plugin:
public class Fail4Plugin implements Plugin<Project> {
private SourceSet sourceSet;
@Override
public void apply(Project project) {
sourceSet = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets().getByName("test");
project.getTasks().register("fail4Task", Fail4Task.class, task -> {
String set = sourceSet.getName();
task.doLast(task1 ->
System.out.println("[run] Project source set: " + set));
});
}
}
Run task fail4Task --configuration-cache --configuration-cache-problems=warn:
Calculating task graph as no cached configuration is available for tasks: fail4Task
> Task :fail4Task
[run] Task source set: main
[run] Project source set: test
2 problems were found storing the configuration cache.
- Task `:fail4Task` of type `ru.vyarus.gradle.plugin.fails.fail4.Fail4Task`: cannot deserialize object of type 'org.gradle.api.tasks.SourceSet' as these are not supported with the configuration cache.
See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types
- Task `:fail4Task` of type `ru.vyarus.gradle.plugin.fails.fail4.Fail4Task`: cannot serialize object of type 'org.gradle.api.internal.tasks.DefaultSourceSet', a subtype of 'org.gradle.api.tasks.SourceSet', as these are not supported with the configuration cache.
See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types
See the complete report at file:///tmp/junit3909871766538813794/build/reports/configuration-cache/ej5hwigx1x3swf7o2d6u9466r/6lf4x0dgeq19zwf53mjr1wh3t/configuration-cache-report.html
[Incubating] Problems report is available at: file:///tmp/junit3909871766538813794/build/reports/problems/problems-report.html
BUILD SUCCESSFU
1 actionable task: 1 executed
Configuration cache entry stored with 2 problems.
Gradle complains only about the task! So the task is serialized completely, but the plugin is not!
It would be easy to proove, just fixing the task:
public class Fail4FixTask extends DefaultTask {
private String name;
public Fail4FixTask() {
SourceSet sourceSet = getProject().getExtensions()
.getByType(JavaPluginExtension.class).getSourceSets().getByName("main");
name = sourceSet.getName();
}
@TaskAction
public void run() {
System.out.println("[run] Task source set: " + name);
}
}
Run fail4Fix --configuration-cache --configuration-cache-problems=warn:
Calculating task graph as no cached configuration is available for tasks: fail4Fix
> Task :fail4Fix
[run] Task source set: main
[run] Project source set: test
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
No problems! So you can use plugin’s private fields to store any data you need, just don’t reference it in runtime blocks (or wrap access with providers, producing something serializable).
Listening tasks
This is not stricly related to the configuration cache, but it would be important to highlight this behavor before the next chapter, covering build services.
Suppose you need to do somethig after a task execution (always), but you don’t control the task (3rd party plugin’s task). You have 2 (conf.cache - legal) ways: use doLast block and build service as listener.
The catch is: doLast block will not be called if task is not executed. Task is not executed when it’s UP-TO-DATEor FROM-CACHE (when build cache enabled). So the only way to always detect task execution is by using build service.
Lets check. Here is a simple build service, listening for tasks:
public abstract class Service implements BuildService<BuildServiceParameters.None>,
OperationCompletionListener {
@Override
public void onFinish(FinishEvent finishEvent) {
System.out.println("[run] Finish event: " + finishEvent.getDescriptor().getName());
}
}
Task with output file (cachable):
public abstract class Sample8Task extends DefaultTask {
@OutputFile
public abstract Property<File> getOut();
@TaskAction
public void run() throws Exception {
System.out.println("[run] Task executed");
File out = getOut().get();
BufferedWriter writer = new BufferedWriter(new FileWriter(out));
writer.append("Sample file content");
writer.close();
}
}
And plugin, declaring the task and configuring doLast block:
public abstract class Sample8Plugin implements Plugin<Project> {
@Inject
public abstract BuildEventsListenerRegistry getEventsListenerRegistry();
@Override
public void apply(Project project) {
// service listen for tasks
final Provider<Service> service = project.getGradle().getSharedServices().registerIfAbsent(
"service", Service.class);
getEventsListenerRegistry().onTaskCompletion(service);
project.getTasks().register("sample8Task", Sample8Task.class, task -> {
task.getOut().set(project.getLayout().getBuildDirectory()
.dir("sample8/out.txt").get().getAsFile());
task.doLast(t -> System.out.println("[run] doLast for sample8Task"));
});
}
}
Run sample8Task --configuration-cache --configuration-cache-problems=warn:
Calculating task graph as no cached configuration is available for tasks: sample8Task
> Task :sample8Task
[run] Task executed
[run] doLast for sample8Task
[run] Finish event: :sample8Task
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed
Configuration cache entry stored.
Both doLast and build service executed.
Run again sample8Task --configuration-cache --configuration-cache-problems=warn:
Reusing configuration cache.
> Task :sample8Task UP-TO-DATE
Finish event: :sample8Task
BUILD SUCCESSFUL in 80ms
1 actionable task: 1 up-to-date
Configuration cache entry reused.
The tasks is UP-TO-DATE and so doLast was not called, but the build service was notified.
The only problem with build service is that it does not provide you a task instance, only task path. So you may need to store some additional task data, collected at configuration phase.
Caching data in build service
Build service cache parameters state at the time of service creation. Any further paramters modifications would not be serializaed (will be lost after the current tasks execution).
Sample service:
public abstract class Service implements BuildService<Service.Params> {
public Service() {
System.out.println("Service created with state: " + getParameters().getValues().get());
}
interface Params extends BuildServiceParameters {
ListProperty<String> getValues();
}
}
Plugin:
public class Sample7Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
final List<String> values = new ArrayList<>();
Provider<Service> service = project.getGradle().getSharedServices().registerIfAbsent(
"service", Service.class, spec -> {
// initial "persisted storage" value
spec.getParameters().getValues().value(values);
});
values.add("val1");
values.add("val2");
project.getTasks().register("sample7Task", task ->
task.doFirst(task1 ->
System.out.println("Task see state: " + service.get().getParameters().getValues().get()))
);
}
}
Service will initialize only at runtime and so the collected configuration state would be preserved.
First run sample7Task --configuration-cache --configuration-cache-problems=warn:
Calculating task graph as no cached configuration is available for tasks: sample7Task
> Task :sample7Task
Service created with state: [val1, val2]
Task see state: [val1, val2]
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
Second run sample7Task --configuration-cache --configuration-cache-problems=warn:
Reusing configuration cache.
> Task :sample7Task
Service created with state: [val1, val2]
Task see state: [val1, val2]
BUILD SUCCESSFUL in 67ms
1 actionable task: 1 executed
Configuration cache entry reused.
State, prepared in plugin during configuration phase was serialized in service parameter.
What is important:
Required state is collected in plugin field
The field is used to initialize service parameter (on creation)
Service is not initializard during configuration phase
In the majority of cases, this trick (almost hack) would not be required becuase you can simply cache the value returned from service instead of re-recovering real service state. For example:
project.getTasks().register("sample7Task", task ->
// would be cached, because its referenced in run block
List<String> state = service.get().getParameters().getValues().get()
task.doFirst(task1 ->
System.out.println("Task see state: " + state))
);
Here service state would be cached and run from cache will not use the service at all.
The case when such caching is not possible is described below (as a real life example).
The real case
The case I faced updating my quality plugin: the plugin is listening for quality tasks (from 3rd party plugins: pmd, checkstyle, findbugs, etc.) and print detected problems into console, using xml reports (produced by quality trasks). As you have seen above, the only way to always print console report after a task is to use build service (all quality tasks are cachable). But build service will receive just a task path, so information about reportable quality tasks must be collected under configuration phase (and cached).
doLast block is called just after the task whereas build service (as task listener) could be called a bit later and doLast it is better for performing output (to print it just below the task output and inside of output of some other task, executed concurrently).
Using both methods (doLast and service) would lead to duplicate output, and so some “execution marker” must be stored somewhere to avoid duplicates. That’s why it would not be possible to just cache service state in a variable (in this case each task would have its own instance of cache and will not see an “execution marker”). So full service state recovery is required.
Simple service, which must contain the required information about tasks:
public abstract class Service implements BuildService<Service.Params>,
OperationCompletionListener {
@Override
public void onFinish(FinishEvent finishEvent) {
if (finishEvent instanceof TaskFinishEvent) {
// not important for example, just showing for completeness
TaskFinishEvent taskEvent = (TaskFinishEvent) finishEvent
String taskPath = taskEvent.descriptor.taskPath;
TaskDesc desc = getParameters().getValues().get().stream()
.filter(it -> it.path.equals(taskPath))
.findFirst().orElse(null)
// do something, knowing additional task data
if (desc != null) {
if (!desc.isCalled()) {
System.out.println("Task " + taskPath + " listened by service");
desc.setCalled(true);
} else {
System.out.println("Task " + taskPath + " listened, but ignored");
}
}
}
}
interface Params extends BuildServiceParameters {
// will hold all required data, prepared by plugin
ListProperty<TaskDesc> getValues();
}
}
TaskDesc is a simpe value class (with properties only):
public class TaskDesc implements Serializable {
private String name;
private String path;
private boolean called;
public TaskDesc() {
}
public TaskDesc(String name, String path) {
this.name = name;
this.path = path;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public boolean isCalled() {
return called;
}
public void setCalled(boolean called) {
this.called = called;
}
@Override
public String toString() {
return name;
}
}
The plugin register task and prepare a collection for caching in service parameter:
public abstract class Sample7Plugin implements Plugin<Project> {
@Inject
public abstract BuildEventsListenerRegistry getEventsListenerRegistry();
// collecting tasks info during configuration phase
private final List<TaskDesc> tasksInfo = new ArrayList<>();
@Override
public void apply(Project project) {
// service 1 with "state" in a private field
Provider<Service> service = project.getGradle().getSharedServices()
.registerIfAbsent("service", Service.class, spec ->
// on first creation, service will cache list value, but it is important
// to not create it too early
spec.getParameters().getValues().set(tasksInfo));
getEventsListenerRegistry().onTaskCompletion(service);
// tasks to demostrate behavior
project.getTasks().register("task1", TrackedTask.class);
project.getTasks().register("task2", TrackedTask.class);
project.getTasks().withType(TrackedTask.class).configureEach(task -> {
captureTaskInfo(task);
task.doLast(task1 -> {
final TaskDesc desc = service.get().getParameters().getValues().get().stream()
.filter(taskDesc -> taskDesc.getPath().equals(task.getPath()))
.findAny().orElse(null);
if (!desc.isCalled()) {
System.out.println("Task " + task1.getName() + " doLast");
desc.setCalled(true);
}
});
});
}
private void captureTaskInfo(Task task) {
System.out.println("Store task descriptor: " + task.getName());
tasksInfo.add(new TaskDesc(task.getName(), task.getPath()));
}
}
Creating cache task1, task2, --configuration-cache, --configuration-cache-problems=warn:
Calculating task graph as no cached configuration is available for tasks: task1 task2
> Configure project :
Service configured: []
Store task descriptor: task1
Store task descriptor: task2
> Task :task1
Service created with state: [task1, task2]
Task task1 doLast
> Task :task2
Task task2 doLast
Task :task1 listened, but ignored
Task :task2 listened, but ignored
BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed
Configuration cache entry stored.
Run from cache task1, task2, --configuration-cache, --configuration-cache-problems=warn:
Reusing configuration cache.
> Task :task1
Service created with state: [task1, task2]
Task task1 doLast
> Task :task2
Task task2 doLast
Task :task1 listened, but ignored
Task :task2 listened, but ignored
BUILD SUCCESSFUL in 74ms
2 actionable tasks: 2 executed
Configuration cache entry reused.
In this example sample task was not cachable and so here doLast was called, but, as you can see, listener is also called and has access to task data (and so in case of cachable task would work correctly).
For sure, this is an edge case, which you, most likely, would never face. Its here just to show what is possible under configuration cache.
Multi-module builds
Configuration cache may force you to use build services, but it is important to keep in mind that you plugin might be used in a multi-module build.
If the plugin from parameters caching sample would be used in the multi-module build like this:
plugins {
id 'com.mycompany.myplugin' version '1.0' apply false
}
subprojects {
apply plugin: 'com.mycompany.myplugin'
}
Then each module would create it’s own plugin instance with it’s own inner state. So the trick like this would not work anymore:
@Override
public void apply(Project project) {
// in multi-module project each module would create this list
final List<String> values = new ArrayList<>();
// but the service is global (initiated just once)
Provider<Service> service = project.getGradle().getSharedServices().registerIfAbsent(
"service", Service.class, spec -> {
spec.getParameters().getValues().value(values);
});
...
}
The service is global, and so only one configuration will be applied (from one subproject) and, as the result, it would contain the state, collected in one subproject only!
The simple workaround is to use separate service instances per project, by modifying service name:
Provider<Service> service = project.getGradle().getSharedServices().registerIfAbsent(
"service" + project.getName(), Service.class, spec -> {
// initial "persisted storage" value
spec.getParameters().getValues().value(values);
});
For example, plugin in subproject “sub” would create service “servicesub”.
Note: you may have problems with the global service only in advanced cases when you have to store state in such service (in parameters, using plugin’s variable).
Different classpaths
Another possible multi-module case is if your plugin would be applied independently in submodules:
// pseudo configuration (in real life it would be different config files)
subrojects("sub1") {
plugins {
id 'com.mycompany.myplugin' version '1.0'
}
}
subrojects("sub2") {
plugins {
id 'com.mycompany.myplugin' version '1.0'
}
}
Then plugins would be loaded in different classpaths and so they would not be able to use a global service (there would be “class cannot be cast to class” error becuase service classes would be different in modules).
Just pay attention to this moment - in most cases, project-scoped service is a better way.
Jococo
When running TestKit-based tests with enabled jococo plugin (for coverage), you'll always have an issue:
1 problem was found storing the configuration cache.
- Gradle runtime: support for using a Java agent with TestKit builds is not yet implemented with the configuration cache.
See https://docs.gradle.org/8.14.3/userguide/configuration_cache.html#config_cache:not_yet_implemented:testkit_build_with_java_agent
But, it's not a critical problem: test must check that it was THE ONLY problem:
BuildResult result = run('someTask', '--configuration-cache', '--configuration-cache-problems=warn');
Assertions.assertThat(result.getOutput()).contains(
"1 problem was found storing the configuration cache",
"Gradle runtime: support for using a Java agent with TestKit",
"Calculating task graph as no cached configuration is available for tasks:"
);
See repository tests for complete configuration cache testing example.
Summary
The summary does not change from part 1.
Just paying additional attention to the “magic wand” for configuration cache compatibilty:
Assigning state to a variable (at correct time) could easilly reduce serialization scope
Provider could “move” code execution from runtime into configuration phase (so let you call not allowed staff at runtime).
ValueSource workarounds caching
In most cases, its simpler to let gradle cache state (with variable or provider), instead of reproducing it under the cache.
it is safier to create build service per plugin instance, instead of global due to potential problems in multi-module projects.
Not allowed gradle projects replacements are perfectly described in the gradle docs.
The only case in my practice that wasn’t described there was project.getAntBuilder() usage in task. In groovy plugin, the solution was to use groovy’s AntBuilder (not gradle builder) directly (new AntBuilder()…).
The runtime objects restrictions mostly driven by configuration state preserved in the disallowed objects, but direct usage of anything (not implicitly aware of gradle state) is completely fine.