Configuration cache должен был стать обязательным в Gradle 9, но требования, в итоге, смягчили. В любом случае, рано или поздно он станет обязательным и авторам плагинов придется его поддерживать.
Я не могу сказать что документация по кэшу плохая, но в ней очень мало практики: каждому разработчику приходится тратить время на эксперементы, пытаясь выяснить как этот кэш работает (а работает он не так как кажется на первый взгляд).
После портирования очередного плагина я решил, наконец, систематизировать знания и написать небольшой гайд на примерах. Для этого я завел специальный репозиторий-песочницу где, исходя из своего опыта, стал тестировать разные аспекты кэша. Естественно, в ходе эксперементов узнал и что-то новое, чем и спешу поделиться. Надеюсь получиться хорошее "погружение" в configuration cache для разработчиков плагинов.
Статья разбита на две части для удобства чтения :
В первой я покажу поведение кэша (теория, которую важно понимать)
Во второй покажу типовые проблемы и их решения (практика)
Методика тестирования
Github репозиторий содержит множество небольших плагинчиков, демонстрирующих разные ситуации. К каждому плагину есть тест, непосредственно проверяющий поведение при включенном кэшэ.
Примеры в статье упрощены для лучшего восприятия. Реальные тесты, как правило, проверяют сразу несколько аспектов (поскольку тесты писались до статьи). В любом случае, имя класса, указанное в статье, соответсвует тесту в репозитории.
Как и любой другой кэш, configuration cache тестируется двумя последовательными запусками: первый - кэширование, второй - работа "из кэша".
Тесты исользуют gradle TestKit, примерно так:
// projectDir - временная директория (создается под каждый тест)
// build.gradle создается вручную (с необходимой конфигурацией)
// создание кэша
BuildResult result = GradleRunner.create()
.withProjectDir(projectDir)
.withArguments(List.of("myTask", "--configuration-cache"))
.withPluginClasspath()
.forwardOutput()
.build();
// проверка что кэш включен
result.getOutput().contains("Calculating task graph as no cached configuration is available for tasks");
// запуск из кэша
result = GradleRunner.create()
.withProjectDir(projectDir)
.withArguments(List.of("myTask", "--configuration-cache"))
.withPluginClasspath()
.forwardOutput()
.build();
// проверка что кэш используется
result.getOutput().contains("Reusing configuration cache");Gradle явно информирует о создании кэша при первом запуске:
Calculating task graph as no cached configuration is available for tasks: sample1Task
…
Configuration cache entry stored.
И о использовании кэша при втором запуске:
Reusing configuration cache
.…
Configuration cache entry reused.
Исполнияемый код
Configuration Cache построен на идее избегания ненужной работы и распараллеливания. Configuration Cache позволяет Gradle полностью пропустить фазу конфигурации, если не менялось ничего что могло повлиять на конфигурацию проекта (сам Gradle скрипт, например). Использование кэша также позволяет Gradle применять дополнительные оптимизации при выполнении задач (tasks).
Это значит что, при включенном кэше, код плагина, относящийся к фазе конфигурации, не будет выполняться.
Возьмем для примера плагин:
public abstract class Sample1Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
System.out.println("[configuration] Plugin applied");
// регистрация таски
project.getTasks().register("sampleTask", task -> {
System.out.println("[configuration] Task configured");
// единственная строка, которая будет работать с включеный кэшем!
task.doFirst(task1 -> System.out.println("[run] Before task"));
});
// afterEvaluate часто используется плагинами, но это тоже фаза конфигурации
project.afterEvaluate(p -> System.out.println("[configuration] Project evaluated"));
}
}*это нормально что плагин абстрактный (далее и все экстеншены) потому что так проще использовать инструментацию gradle (для внедрения property).
Первое выполнение: sampleTask --configuration-cache
Calculating task graph as no cached configuration is available for tasks: sampleTask
> Configure project :
[configuration] Plugin applied
[configuration] Project evaluated
[configuration] Task configured
> Task :sampleTask
[run] Before task
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.Второй запуск (кэш используется): sampleTask --configuration-cache
Reusing configuration cache.
> Task :sampleTask
[run] Before task
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry reused.Как можно видеть, при работе из кэша, код, относящийся к фазе конфигурации не выполняется.
Состояние
Configuration cache сериализует состояние (configuration) проекта и переиспользует его. Это касается и кода и конфигурации в билд скрипте.
Для демонстрации возьмем простое расширение:
public class Sample1Extension {
public String message = "Default";
public Sample1Extension() {
System.out.println("[configuration] Extension created")
}
public String getMessage() {
// нет указания фазы ([run]) потомучто может быть вызвано в любой
System.out.println("Extension get message: " + message);
return message;
}
}Плагин с задачей, использующей расширение (ис��ользует конфигурацию пользователя из скрипта):
public abstract class Sample1Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
System.out.println("[configuration] Plugin executed");
Sample1Extension ext = project.getExtensions()
.create("sample1", Sample1Extension.class);
project.getTasks().register("sampleTask", task -> {
task.doFirst(task1 -> System.out.println("[run] User message: "
+ ext.message));
});
}
}Допустим build.gradle у нас такой:
sample1 {
message = "hello user!"
}Первое выполнение: sampleTask --configuration-cache
Calculating task graph as no cached configuration is available for tasks: sampleTask
> Configure project :
[configuration] Plugin executed
[configuration] Extension created
> Task :sampleTask
Extension get message: hello user!
[run] User message: hello user!
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.Второй запуск (из кэша): sampleTask --configuration-cache
Reusing configuration cache.
> Task :sampleTask
Extension get message: hello user!
[run] User message: hello user!
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry reused.Обращаю внимание: при выполнении из кэша, конструктор расширения не зовется, при этом геттер исполняется.
Что произошло: при первом выполнении Gradle честно исполняет весь код, но потом начинает анализировать получившиеся объекты: он видит что ext.message используется в рантайм блоке и сериализует весь екстеншен (ext). При повторном выполнении (из кэша), объекты десериализуются (и поэтому конструкторы не вызываются).
Логичный вопрос: а что будет если пользователь поменят конфигурацию в скрипте:
sample1 {
message = "changed message!"
}Запускаем (кэш уже сохранен): sampleTask --configuration-cache
Calculating task graph as configuration cache cannot be reused because file 'build.gradle' has changed.
> Configure project :
[configuration] Plugin executed
[configuration] Extension created
> Task :sampleTask
Extension get message: changed message!
[run] User message: changed message!
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.Gradle заметил изменение скрипта и инвалидировал кэш, п.э. фаза конфигурации была повторена.
Поля плагина
А что будет если плагин использует поля класса (fields)?
В качестве примера возьмем задачу, которая использует поле плагина в рантайм блоке:
public abstract class Sample1Plugin implements Plugin<Project> {
private String pluginField;
public Sample1Plugin() {
System.out.println("[configuration] Plugin created");
}
@Override
public void apply(Project project) {
project.afterEvaluate(p -> {
pluginField = "assigned value";
System.out.println("[configuration] Project evaluated");
});
project.getTasks().register("sampleTask", task -> {
task.doFirst(task1 -> System.out.println("[run] Before task: "
+ pluginField));
});
}
}Первый запуск: sampleTask --configuration-cache
Calculating task graph as no cached configuration is available for tasks: sample1Task
> Configure project :
[configuration] Plugin created
[configuration] Project evaluated
> Task :sampleTask
[run] Before task: assigned value
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.Запуск из кэша: sampleTask --configuration-cache:
Reusing configuration cache.
> Task :sample1Task
[run] Before task: assigned value
BUILD SUCCESSFUL in 61ms
1 actionable task: 1 executed
Configuration cache entry reused.Как можно видеть, поле плагина было сериализовано и значение доступно при работе из кэша.
Поля задачи
Теперь посмотрим как поля (fields) задачи (task) переживают сериализацию:
public abstract class Sample1Task extends DefaultTask {
@Input
abstract Property<String> getMessage();
@Input
abstract Property<String> getMessage2();
public String field;
private String privateField;
public Sample1Task() {
System.out.println("[configuration] Task created");
// инициализация приватного поля (важно - это фаза конфигурации!)
privateField = "set";
}
@TaskAction
public void run() {
System.out.println("[run] Task executed: message=" + getMessage().get()
+ ", message2=" + getMessage2().get()
+ ", public field=" + field
+ ", private field=" + privateField);
}
}Для полноты картины используются gradle property и локальные поля класса (публичное и приватное). Плагин, инициализирующий задачу:
public abstract class Sample1Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
System.out.println("[configuration] Plugin applied");
final Sample1Extension ext = project.getExtensions()
.create("sample1", Sample1Extension.class);
// регистрация задачи
project.getTasks().register("sample1Task", Sample1Task.class, task -> {
task.getMessage().convention(ext.message);
task.getMessage2().convention("Default");
// публичное поле инициализировано напрямую
task.field = "assigned value";
});
// отложенная (lazy) конфигурация
project.getTasks().withType(Sample1Task.class).configureEach(task -> {
// переопределение дефолтного значения
task.getMessage2().set("Custom");
});
}
}messageприсваивается значение из расширения (то что конфигурируется в билд скрипте)message2присваивается дефолтное "Default" а затем переопределяется в блоке отложенной конфигурацииПубличное поле
fieldинициализируется в "assigned value"Приватное поле выставляется в конструкторе задачи в значение "set"
Расширение такое же как в предидущем примере. Конфигурация:
sample1 {
message = "hello user!"
}Запускаем два раза sample1Task --configuration-cache (интересен только запуск из кэша):
Reusing configuration cache.
> Task :sample1Task
Extension get message: hello user!
[run] Task executed: message=hello user!, message2=Custom, public field=assigned value, private field=set
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry reused.Как можно видеть, все поля задачи были сериализованы. Так что использование полей класса вполне совместимо с configuration cache.
Важно: только значения, присвоенные на этапе конфигурации будут сохранены! Детальнее будет показано во второй статье.
Сломанная уникальность
Сериализация конфигурации не могла не иметь последствий: самая популярная проблема - сломанная уникальность объектов. Допустм, какой-то объект используется в разных рантайм блоках - после десериализации каждый блок будет ссылаться на свой собственный объект.
Для примера возьмем объект, который будет использоваться двумя задачами:
ublic class SharedState {
// show how externally assigned values survive
public String direct;
public List<String> list = new ArrayList<>();
public SharedState() {
System.out.println("[configuration] Shared state created: " + System.identityHashCode(this));
}
@Override
public String toString() {
return System.identityHashCode(this) + "@" + list.toString() + ", direct=" + direct;
}
}*System.identityHashCode в toString покажет уникальность объекта
Плагин:
public abstract class Sample2Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// общий объект для двух задач
final SharedState state = new SharedState();
state.direct = "Custom";
project.getTasks().register("task1").configure(task ->
task.doLast(task1 -> {
state.list.add("Task 1");
System.out.println("[run] Task 1 shared object: " + state);
}));
project.getTasks().register("task2").configure(task ->
task.doLast(task1 -> {
state.list.add("Task 2");
System.out.println("[run] Task 2 shared object: " + state);
}));
}
}Сначала запустим не включая кэшtask1 task2:
> Configure project :
[configuration] Shared state created: 1353516483
> Task :task1
[run] Task 1 shared object: 1353516483@[Task 1], direct=Custom
> Task :task2
[run] Task 2 shared object: 1353516483@[Task 1, Task 2], direct=Custom
BUILD SUCCESSFUL
2 actionable tasks: 2 executedОдин объект используется в обеих задачах (1353516483).
Теперь запустим с включеным кэшем: task1 task2 --configuration-cache
Calculating task graph as no cached configuration is available for tasks: task1 task2
> Configure project :
[configuration] Shared state created: 1202973804
> Task :task2
[run] Task 2 shared object: 1650372210@[Task 2], direct=Custom
> Task :task1
[run] Task 1 shared object: 724264550@[Task 1], direct=Custom
BUILD SUCCESSFUL
2 actionable tasks: 2 executed
Configuration cache entry stored.И уже при первом выполнении (запись кэша только создается) можно увидеть разницу: теперь каждая задача использует свой собственный экземпляр объекта!
Т.е., несмотря на то что при создании записи кэша фаза конфигурации выполняется, рантайм уже работает с десериализованными объектами!
Если запустить еше раз (из кэша) task1 task2 --configuration-cache:
Reusing configuration cache.
> Task :task1
[run] Task 1 shared object: 1796870189@[Task 1], direct=Custom
> Task :task2
[run] Task 2 shared object: 1139607728@[Task 2], direct=Custom
BUILD SUCCESSFUL
2 actionable tasks: 2 executed
Configuration cache entry reused.Мы опять видим разные экземпляры объекта используемые в задачах.
Итого: первый запуск с включенным configuration cache не тоже самое что обычный запуск. Но это, на самом деле, совсем не плохо: поскольку Gradle делает сериализацию и десериализацию объектов уже на первом запуске, мы узнаем о проблемах сериализации сразу! Это сильно облегчает поиск проблем.
Build сервисы
В идеале, задачи должны работать только со свом состоянием. В реальной жизни, к сожалению, бывают случаи когда задачи должны коммуницировать и уникальные объекты просто необходимы.
Единственный способ обойти "сломанную уникальность" объектов - использование build сервисов.
public abstract class SharedService implements BuildService<SharedService.Params>, AutoCloseable {
public String extParam;
// задачи могут выполняться параллельно! (избегаем ConcurrentModificationException)
public List<String> list = new CopyOnWriteArrayList<>();
public SharedService() {
// может произойти как в рантайме так и при конфигурации
System.out.println("Shared service created " + System.identityHashCode(this) + "@");
}
public interface Params extends BuildServiceParameters {
Property<String> getExtParam();
}
@Override
public String toString() {
return System.identityHashCode(this) + "@" + list.toString()
+ ", param: " + getParameters().getExtParam().getOrNull()
+ ", field: " + extParam;
}
// ВАЖНО: gradle может закрыть сервис в любой момент и стартовать новый интсанс!
@Override
public void close() throws Exception {
System.out.println("Shared service closed: " + System.identityHashCode(this));
}
}Плагин, регистрирующий сервис и две задачи, использующие сервис:
public abstract class Sample3Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// ВАЖНО: в этот момент сервис не создается!
final Provider<SharedService> service = roject.getGradle().getSharedServices()
.registerIfAbsent("service", SharedService.class, spec -> {
// значение, присваемое при создании сервиса
spec.getParameters().getExtParam().convention("Default");
});
// значение присваивается ПОСЛЕ создания сервиса
project.afterEvaluate(p -> {
service.get().extParam = "Custom";
System.out.println("[configuration] Project evaluated");
});
project.getTasks().register("task1").configure(task ->
task.doLast(task1 -> {
final SharedService sharedService = service.get();
sharedService.list.add("Task 1");
System.out.println("[run] Task 1 shared object: " + sharedService);
}));
project.getTasks().register("task2").configure(task -> {
// убераем одновременное выполнение задач для простоты
task.mustRunAfter("task1");
task.doLast(task1 -> {
final SharedService sharedService = service.get();
sharedService.list.add("Task 2");
System.out.println("[run] Task 2 shared object: " + sharedService);
});
});
}
}Запуск с выключенным кэшем task1 task2:
> Configure project :
Shared service created 1202901166@
[configuration] Project evaluated
> Task :task1
[run] Task 1 shared object: 1202901166@[Task 1], param: Default, field: Custom
> Task :task2
[run] Task 2 shared object: 1202901166@[Task 1, Task 2], param: Default, field: Custom
Shared service closed: 1202901166
BUILD SUCCESSFUL
2 actionable tasks: 2 executedПлагин инициирует создание сервиса в блоке afterEvaluate (фаза конфигурации) чтобы присвоить значение в поле класса напрямую:
service.get().extParam = "Custom";
Один и тот же объект сервиса используется и в обоих фазах и в обоих задачах (1202901166).
Теперь запускаем с включенным кэшэм task1 task2 --configuration-cache:
Calculating task graph as no cached configuration is available for tasks: task1 task2
> Configure project :
Shared service created 644777498@
[configuration] Project evaluated
Shared service closed: 644777498
> Task :task1
Shared service created 1665614489@
[run] Task 1 shared object: 1665614489@[Task 1], param: Default, field: null
> Task :task2
[run] Task 2 shared object: 1665614489@[Task 1, Task 2], param: Default, field: null
Shared service closed: 1665614489
BUILD SUCCESSFUL
2 actionable tasks: 2 executed
Configuration cache entry stored.Поведение изменилось:
Как и раньше, сервис создается в блоке afterEvaluate. Но сервис закрывается сразу после фазы конфигурации! Это абсолютно нормальное поведение gradle с вкоюченным кэшем - вообще, они советуют использовать сервисы ТОЛЬКО в рантайме и специально закрывают заранее созданные инстансы, чтобы выявить побочные эффекты.
Из-за закрытия сервиса, состояние поля класса (назначенное в afterEvaluate) был потеряно! Состояние сервиса не сериализуется! Только значения назначенные при создании сервиса запоминаются (
specблок вregisterIfAbsent).
Как можно видеть, обе задачи используют один и тот же объект сервиса (правда не тот же самый что был на этапе конфигурации).
И, наконец, запускаем из кэша task1 task2 --configuration-cache:
Reusing configuration cache.
> Task :task1
Shared service created 1651282902@
[run] Task 1 shared object: 1651282902@[Task 1], param: Default, field: null
> Task :task2
[run] Task 2 shared object: 1651282902@[Task 1, Task 2], param: Default, field: null
Shared service closed: 1651282902
BUILD SUCCESSFUL
2 actionable tasks: 2 executed
Configuration cache entry reused.Тот же самый объект сервиса используется обоими задачами. Поскольку поля сервиса не сериализуются, значение extParam было утеряно.
Таким образом: можно быть уверенным в уникальности сервиса при включенном configuration cache (рантайм фаза). Сервис, используемый в фазе конфигурации, будет закрыт.
Как не дать сервису закрыться
Как правило, в этом нет никаго смысла, потому что даже если один объект сервиса будет в обоих фазаз при создании кэша, при работе из кэша фазы конфигурации то не будет. Кроме того, изменения состояния сервиса не будет сериализовано (если менять значения property после создания сервиса).
Однако, как будет показано во второй статье, значения сервиса можно сериализовать напрямую (закэшировав данные сервиса в рантайм блоке). В этом случае, уникальность сервиса важна для подготовки уникального состояния (это очень специфичные случаи).
Так же, не стоит забывать что gradle может закрыть сервис и в фазе рантайма, если посчитает что он больше не используется (это может проявится в сложных случаях, когда gradle не может явно понять что сервис используется в последующих задачах).
Gradle не будет закрывать сервис, который слушает выполнение задач. Для этого сервис должен реализовать OperationCompletionListener
ublic abstract class SharedService implements BuildService<SharedServiceSingleton.Params>, AutoCloseable,
OperationCompletionListener {
...
@Override
public void onFinish(FinishEvent finishEvent) {
System.out.println("Finish event: " + finishEvent.getDescriptor().getName() + " caught on service " + this);
}
}(в остальном сервис такой же как и в предыдущем примере)
Плагин должен зарегистрировать сервис в качестве "слушателя":
public abstract class Sample3Plugin implements Plugin<Project> {
@Inject
public abstract BuildEventsListenerRegistry getEventsListenerRegistry();
@Override
public void apply(Project project) {
...
final Provider<SharedServiceSingleton> service = project.getGradle().getSharedServices().registerIfAbsent(
"service", SharedServiceSingleton.class, spec -> {
spec.getParameters().getExtParam().convention("Default");
});
// сервис вызывается после каждой выполненной задачи, что предотвращает его закрытие
getEventsListenerRegistry().onTaskCompletion(service);
...
}
}Создание кэша: task1 task2 --configuration-cache
Calculating task graph as no cached configuration is available for tasks: task1 task2
> Configure project :
Shared service created 331815390@
[configuration] Project evaluated
> Task :task1
[run] Task 1 shared object: 331815390@[Task 1], param: Default, field: Custom
Finish event: :task1 caught on service 331815390@[Task 1], param: Default, field: Custom
> Task :task2
[run] Task 2 shared object: 331815390@[Task 1, Task 2], param: Default, field: Custom
Finish event: :task2 caught on service 331815390@[Task 1, Task 2], param: Default, field: Custom
Shared service closed: 331815390
BUILD SUCCESSFUL
2 actionable tasks: 2 executed
Configuration cache entry stored.На этот раз сервис не закрывается после фазы конфигурации и тот же самый объект используется в рантайме (значение поля, назначенное напряму плагином сохраняется).
Работа из кэша task1 task2 --configuration-cache:
Reusing configuration cache.
> Task :task1
Shared service created 1976530883@
[run] Task 1 shared object: 1976530883@[Task 1], param: Default, field: null
Finish event: :task1 caught on service 1976530883@[Task 1], param: Default, field: null
> Task :task2
[run] Task 2 shared object: 1976530883@[Task 1, Task 2], param: Default, field: null
Finish event: :task2 caught on service 1976530883@[Task 1, Task 2], param: Default, field: null
Shared service closed: 1976530883
BUILD SUCCESSFUL in 53ms
2 actionable tasks: 2 executed
Configuration cache entry reused.Как и раньше, один объект сервиса используется в обоих задачах, но поле, назначенное после создания сервиса, не сохранено (состояние сервиса не сериализуется).
Вызовы методов
Сейчас мы посмотрим будут ли работать вызовы методов из рантайм блоков при включенном кэше:
public class Sample4Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getTasks().register("task1").configure(task -> {
task.doLast(task1 -> {
System.out.println("[run] Task exec: " + computeMessage("static"));
});
});
}
private String computeMessage(String source) {
System.out.println("called computeMessage('" + source + "')");
return "computed message " + source;
}
}Создание кэша task1 --configuration-cache :
Calculating task graph as no cached configuration is available for tasks: task1
> Task :task1
called computeMessage('static')
[run] Task exec: computed message static
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.Работа из кэша task1 --configuration-cache :
Reusing configuration cache.
> Task :task1
called computeMessage('static')
[run] Task exec: computed message static
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry reused.Как видно из примера, метод вызывается без проблем. Обратите внимание: метод не статический и, притом, приватный!
Таким образом, ограничений на вызовы методов при использовании кэша нет.
Должен заметить что есть проблемы у плагинов, написанных на groovy, но это из-за динамической сушности groovy. Примеров не буду показывать, но сталкивался неоднократно: обходится, как правило, открытием метода (public) и переводом его в static.
Провайдеры
Провайдеры это настоящяя "волшебная палочка" для обхода проблем с configuration cache. С помошью провайдера можно легко обойти ограничения на использования запрещенных объектов (пример покажу в следующей статье).
Сейчас посмотрим только как провайдер кэшируется:
public class Sample4Plugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getTasks().register("task1").configure(task -> {
Provider<String> provider = project.provider(() -> {
String res = String.valueOf(project.findProperty("param"));
System.out.println("Provider called: " + res);
return res;
});
task.doLast(task1 -> {
System.out.println("[run] Task exec: " + provider.get());
});
});
}
}В этом примере провайдер использует project property только чтобы показать инвалидацию кэша.
Для начала запустим без кэша task1 -Pparam=1:
> Task :task1
Provider called: 1
Task exec: 1
BUILD SUCCESSFUL
1 actionable task: 1 executedКак и ожидалось, провайдер был вызван в рантайме.
Инициализируем кэш task1 -Pparam=1 --configuration-cache :
Calculating task graph as no cached configuration is available for tasks: task1
Provider called: 1
> Task :task1
[run] Task exec: 1
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.Обратите внимание: провайдер был вызван в фазе конфигурации!
Работа из кэша task1 -Pparam=1 --configuration-cache :
Reusing configuration cache.
> Task :task1
[run] Task exec: 1Провайдер не зовется! На этапе записи кэша провайдер был вызван в фазе конфигурации чтобы кэшировать его значение! А это значит, внутри провайдера можно использовать все что угодно (любые объекты недоступные в рантайме)!!!
Теперь поменяем значение параметра task1 -Pparam=2 --configuration-cache :
Calculating task graph as configuration cache cannot be reused because the set of Gradle properties has changed: the value of 'param' was changed.
Provider called: 2
> Task :task1
[run] Task exec: 2
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.Gradle инвалидировал кэш. Справедливости ради, это произршло не потому что gradle понял что изменился параметр используемый в рантайме, а просто что-то явно поменялось - значит лучше сбросить кэш (то же самое поведение как и при изменении файла билд скрипта).
Некэшируемые провайдеры
У gradle есть специальные некэшируемые провайдеры: ValueSource (упоминаются в документации). В отличии от простых провайдеров, ValueSource вызывается даже при работе из кэша.
Пример:
public abstract class NonCacheableValue implements ValueSource<String, ValueSourceParameters.None> {
@Override
public @Nullable String obtain() {
String val = System.getProperty("foo");
System.out.println("NonCacheableValue: " + val);
return val;
}
}На этот раз используется system property, которую gradle не отслеживает для сброса кэша.
Плагин:
public class Sample4ValuePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getTasks().register("task1").configure(task -> {
final Provider<String> provider = project.getProviders()
.of(NonCacheableValue.class, spec -> {});
task.doLast(task1 -> {
System.out.println("[run] Task exec: " + provider.get());
});
});
}
}Инициализируем кэш task1 -Dfoo=1 —configuration-cache:
Calculating task graph as no cached configuration is available for tasks: task1
> Task :task1
NonCacheableValue: 1
[run] Task exec: 1
BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.Обратите внимание: ValueSource вызван в рантайме.
Запуск из кэша task1 -Dfoo=1 —configuration-cache:
Reusing configuration cache.
> Task :task1
NonCacheableValue: 1
[run] Task exec: 1
BUILD SUCCESSFUL in 73ms
1 actionable task: 1 executed
Configuration cache entry reused.И опять ValueSource был вызван.
Теперь меняем значение system property task1 -Dfoo=2 —configuration-cache:
Reusing configuration cache.
> Task :task1
NonCacheableValue: 2
[run] Task exec: 2
BUILD SUCCESSFUL in 46ms
1 actionable task: 1 executed
Configuration cache entry reused.Кэш не был инвалидирован, но правильное значение использовано!
Выводы
После включения configuration cache, gradle начинает анализировать рантайм блоки и сериализовать необходимое состояние (подготовленное в фазе конфигурации):
В фазе конфигурации нет никаких ограничений при включении кэша
При работе из кэша, ни плагин ни задачи ни расширения не будут созданы через конструктор, вместо этого они будут десириализованы
Состояние полей плагина и задачи сериализуется (но не изменения в фазе рантайма)
Gradle сериализует объекты необходимые рантайм блокам, и не имеет значения объект ли это созданный в плагине или же объект конфигурации из скрипта.
Нельзя полагаться на уникальность объектов после десериализации
Build сервисы:
Единственный способ для обмена состоянием между задачами
Не сериализуются (только изначальная конфигурация запоминается)
Могут закрыться в любое время.
Только сервисы, слушаюшие завершение задач не закрываются
Методы, вызываемые из рантайм блоков, зовутся даже при работе из кэша
При включенном кэшэ Provider вызывается на этапе конфигурации и значит он не подподает под ограничения рантайм блоков. Значение кэшируется.
Значение ValueSource никогда не кэшируется
Gradle старается следить за изменениями в проекте и инвалидирует кэш. Конечно, есть пограничные случаи.
В части 2 будет показано практическое применение этой теории для написания / изменения плагинов.
Забегая вперед, скажу - проверяйте теории прежде чем переделывать весь плагин. Можете использовать мой репозиторий для эксперементов (ищите подходящий пример и модифицируйте под свои нужды чтобы быть уверенным в том как себя поведет Gradle).
P.S. Наверняка будет вопрос почему нет плашки "перевод", ведь ссылки на английские версии статей стоят в репозитории: изначально планировал написать сухой английский вариант у себя в блоге и сделать расширенную версию для хабра. К сожалению, времени на статьи для хабра сразу не хватило. В итоге, статьи выходят с опозданием, но доработанные.
