Sharing objects across taks with Configuration Cache enabled

Hi,

I’m running into an issue with objects that are shared across multiple tasks.

A Plugin creates a TestListener that is used by a custom task and the test tasks in the project:

class ExamplePlugin implements org.gradle.api.Plugin<Project> {

    @Override
    public void apply(Project project) {
        var testListener = new ExampleTestListener();

        project.getTasks().register("exampleTask", ExampleTask.class, task -> {
            task.getTestListener().set(testListener);
        });
        project.getTasks().withType(Test.class, testTask -> {
            testTask.finalizedBy("exampleTask");
            testTask.addTestListener(testListener);
        });
    }

}

ExampleTestListener keeps track of the tests that have been executed:

class ExampleTestListener implements TestListener {
    
    List<TestDescriptor> tests = new ArrayList<>();

    ...
 
    @Override
    public void afterTest(TestDescriptor testDescriptor, TestResult testResult) {
        System.err.println("[ExampleTestListener] Test task uses instance %s".formatted(this));
        tests.add(testDescriptor);
    }
}

ExampleTask logs the data collected by ExampleTestListener:

abstract class ExampleTask extends DefaultTask {

    @Internal
    abstract Property<ExampleTestListener> getTestListener();

    @Inject
    public ExampleTask() {
        doLast(task -> {
            getLogger().lifecycle("[ExampleTask] Referenced ExampleTestListener instance: %s".formatted(getTestListener().get()));
            getLogger().lifecycle("[ExampleTask] Test count: %s".formatted(getTestListener().get().tests.size()));
        });
    }

}

Execution (Configuration Cache disabled)

./gradlew clean test --no-configuration-cache

> Task :test
[ExampleTestListener] Test task uses instance com.example.ExampleTestListener@60e6df11

FooTest > testSomething() PASSED

> Task :exampleTask
[ExampleTask] Referenced ExampleTestListener instance: com.example.ExampleTestListener@60e6df11
[ExampleTask] Test count: 1

Execution (Configuration Cache enabled)

./gradlew clean test --configuration-cache        
Calculating task graph as no configuration cache is available for tasks: clean test

> Task :test
[ExampleTestListener] Test task uses instance com.example.ExampleTestListener@51c45d55

FooTest > testSomething() PASSED

> Task :exampleTask
[ExampleTask] Referenced ExampleTestListener instance: com.example.ExampleTestListener@1fcdb88
[ExampleTask] Test count: 0

With Configuration Cache enabled, the Test tasks and ExampleTask reference different instances:

  • ExampleTestListener@51c45d55
  • ExampleTestListener@1fcdb88

This causes ExampleTask to read 0 tests being executed.

Sample repo: GitHub - skippy-io/gradle-configuration-cache-issue: Sample project to demonstrate an issue with Gradle's Configuration Cache

How can I ensure that ExampleTasks sees the data written by the test tasks when Configuration Cache is enabled?

Thanks in advance!

With configuration cache the state of each task is serialized and persisted and then deserialized for usage.
During this deserialization both tasks of course get different instances created.

Instead use a shared build service for sharing data between tasks that are not shared via task outputs / inputs.

1 Like

Thank you for the response. Just read up on shared build services - that indeed sounds like the solution for my problem.

Before I go down the route of a shared build service: Can this be solved by customizing the (de-)serialization that is used by the build cache? It seems odd that the serialization ignores instance fields.

What do you mean with “it ignores instance fields”?

The test task adds data to the tests field:

My assumption how this should work:

  • ExampleTestListener$12345 is created
  • Test task modifies the tests field in ExampleTestListener$12345
  • ExampleTestListener$12345 is serialized (including the tests field)
  • ExampleTestListener$67890 is de-serialized (tests field is restored)
  • ExampleTask checks ExampleTestListener$67890 and sees the data written by the Test task

Hope that clears it up.

It clears up what you understand, yes, but that’s not how it happens.

First all up to and including the configuration phase is executed.
Then the state of the tasks is serialized into the configuration cache, so that all that is executed up to now can be skipped for further invocations.

At this point in time the test task did not run and did not write any instance fields.
The instances in both tasks are the same but both with empty tests fields.

If the state of tasks is then deserialized from the CC, both tasks get individual instances deserialized, both with empty lists.

1 Like

Thanks for the detailed explanation. I will take a stab at your proposed solution (shared build service).

Your feedback has been very helpful :+1:

1 Like