Skip to content

Commit

Permalink
Cleanup stale output files during execution (#2572)
Browse files Browse the repository at this point in the history
We now keep a registry of all the outputs generated by Gradle which will be reset on each version change. If Gradle encounters an existing output file, then it will remove it if is not part of the registered outputs and owned by Gradle/the build. We do also not remove directories containing outputs from different tasks.
The check to delete the stale outputs happens now just before the task executes and not up-front as it did before.
The `build` directory and all delete targets of the `clean` task are registered as owned by Gradle and are considered safe to delete.
Currently, the set of recorded task output files is only growing if we do not change the Gradle version. In the future we can improve on this by also detecting that some directories where removed (e.g. by running a clean task) and reflect this in the registry.

Fixes #1168
Fixes #973
+review REVIEW-6557
  • Loading branch information
wolfs committed Jul 28, 2017
1 parent bd2de06 commit dbf5680
Show file tree
Hide file tree
Showing 32 changed files with 957 additions and 752 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ class IncrementalTasksIntegrationTest extends AbstractIntegrationSpec {
}
}
if (!inputs.incremental) {
createOutputsNonIncremental()
}
touchOutputs()
discoveredFiles = inputs.getDiscoveredInputs()
Expand All @@ -94,6 +98,9 @@ class IncrementalTasksIntegrationTest extends AbstractIntegrationSpec {
def touchOutputs() {
}
def createOutputsNonIncremental() {
}
def addedFiles = []
def changedFiles = []
def removedFiles = []
Expand All @@ -114,6 +121,12 @@ class IncrementalTasksIntegrationTest extends AbstractIntegrationSpec {
@OutputDirectory
def File outputDir
@Override
def createOutputsNonIncremental() {
new File(outputDir, 'file1.txt').text = 'outputFile1'
new File(outputDir, 'file2.txt').text = 'outputFile2'
}
@Override
def touchOutputs() {
outputDir.eachFile {
Expand Down Expand Up @@ -184,7 +197,7 @@ class IncrementalTasksIntegrationTest extends AbstractIntegrationSpec {
file('inputs/file1.txt') << "changed content"

then:
executesWithIncrementalContext("ext.changed = ['file1.txt']");
executesWithIncrementalContext("ext.changed = ['file1.txt']")
}

def "incremental task is informed of 'out-of-date' files when discovered input file modified"() {
Expand Down Expand Up @@ -435,7 +448,7 @@ ext.added = ['file3.txt', 'file4.txt']
failedExecution()

then:
executesWithIncrementalContext("ext.changed = ['file1.txt']");
executesWithIncrementalContext("ext.changed = ['file1.txt']")
}
/*
7. Sad-day cases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.gradle.api.internal.changedetection.state.FileCollectionSnapshotterRegistry;
import org.gradle.api.internal.changedetection.state.TaskExecution;
import org.gradle.api.internal.changedetection.state.TaskHistoryRepository;
import org.gradle.api.internal.changedetection.state.TaskOutputFilesRepository;
import org.gradle.api.internal.changedetection.state.ValueSnapshotter;
import org.gradle.api.internal.file.FileCollectionFactory;
import org.gradle.api.tasks.incremental.IncrementalTaskInputs;
Expand All @@ -51,18 +52,20 @@ public class DefaultTaskArtifactStateRepository implements TaskArtifactStateRepo
private final ClassLoaderHierarchyHasher classLoaderHierarchyHasher;
private final TaskCacheKeyCalculator cacheKeyCalculator;
private final ValueSnapshotter valueSnapshotter;
private final TaskOutputFilesRepository taskOutputFilesRepository;

public DefaultTaskArtifactStateRepository(TaskHistoryRepository taskHistoryRepository, Instantiator instantiator,
FileCollectionSnapshotterRegistry fileCollectionSnapshotterRegistry,
FileCollectionFactory fileCollectionFactory, ClassLoaderHierarchyHasher classLoaderHierarchyHasher,
TaskCacheKeyCalculator cacheKeyCalculator, ValueSnapshotter valueSnapshotter) {
TaskCacheKeyCalculator cacheKeyCalculator, ValueSnapshotter valueSnapshotter, TaskOutputFilesRepository taskOutputFilesRepository) {
this.taskHistoryRepository = taskHistoryRepository;
this.instantiator = instantiator;
this.fileCollectionSnapshotterRegistry = fileCollectionSnapshotterRegistry;
this.fileCollectionFactory = fileCollectionFactory;
this.classLoaderHierarchyHasher = classLoaderHierarchyHasher;
this.cacheKeyCalculator = cacheKeyCalculator;
this.valueSnapshotter = valueSnapshotter;
this.taskOutputFilesRepository = taskOutputFilesRepository;
}

public TaskArtifactState getStateFor(final TaskInternal task) {
Expand Down Expand Up @@ -183,6 +186,7 @@ public void afterTask(Throwable failure) {
// Only store new taskState if there was no failure, or some output files have been changed
if (failure == null || taskState.hasAnyOutputFileChanges()) {
history.update();
taskOutputFilesRepository.recordOutputs(history.getCurrentExecution());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2017 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.api.internal.changedetection.state;

import org.gradle.cache.PersistentCache;
import org.gradle.cache.PersistentIndexedCache;
import org.gradle.cache.PersistentIndexedCacheParameters;
import org.gradle.internal.nativeintegration.filesystem.FileType;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;

public class DefaultTaskOutputFilesRepository implements TaskOutputFilesRepository, Closeable {

private final PersistentCache cacheAccess;
private final FileSystemMirror fileSystemMirror;
private final PersistentIndexedCache<String, Boolean> outputFiles; // The value is true if it is an output file, false if it is a parent of an output file

public DefaultTaskOutputFilesRepository(PersistentCache cacheAccess, FileSystemMirror fileSystemMirror, InMemoryCacheDecoratorFactory inMemoryCacheDecoratorFactory) {
this.cacheAccess = cacheAccess;
this.fileSystemMirror = fileSystemMirror;
this.outputFiles = cacheAccess.createCache(cacheParameters(inMemoryCacheDecoratorFactory));
}

@Override
public boolean isGeneratedByGradle(File file) {
File absoluteFile = file.getAbsoluteFile();
return containsFilesGeneratedByGradle(absoluteFile) || isContainedInAnOutput(absoluteFile);
}

private Boolean isContainedInAnOutput(File file) {
File currentFile = file;
while (currentFile != null) {
if (outputFiles.get(currentFile.getPath()) == Boolean.TRUE) {
return true;
}
currentFile = currentFile.getParentFile();
}
return false;
}

private boolean containsFilesGeneratedByGradle(File file) {
return outputFiles.get(file.getPath()) != null;
}

@Override
public void recordOutputs(TaskExecution taskExecution) {
for (String outputFilePath : taskExecution.getDeclaredOutputFilePaths()) {
FileSnapshot fileSnapshot = fileSystemMirror.getFile(outputFilePath);
File outputFile = new File(outputFilePath);
boolean exists = fileSnapshot == null ? outputFile.exists() : fileSnapshot.getType() != FileType.Missing;
if (exists) {
outputFiles.put(outputFilePath, Boolean.TRUE);
File outputFileParent = outputFile.getParentFile();
while (outputFileParent != null) {
String parentPath = outputFileParent.getPath();
if (outputFiles.get(parentPath) != null) {
break;
}
outputFiles.put(parentPath, Boolean.FALSE);
outputFileParent = outputFileParent.getParentFile();
}
}
}
}

private static PersistentIndexedCacheParameters<String, Boolean> cacheParameters(InMemoryCacheDecoratorFactory inMemoryCacheDecoratorFactory) {
return new PersistentIndexedCacheParameters<String, Boolean>("outputFiles", String.class, Boolean.class)
.cacheDecorator(inMemoryCacheDecoratorFactory.decorator(100000, true));
}

@Override
public void close() throws IOException {
cacheAccess.close();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* Copyright 2017 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.
Expand All @@ -14,11 +14,12 @@
* limitations under the License.
*/

package org.gradle.internal.cleanup;
package org.gradle.api.internal.changedetection.state;

import java.io.File;

public interface BuildOutputDeleter {
public interface TaskOutputFilesRepository {
boolean isGeneratedByGradle(File file);

void delete(Iterable<File> outputs);
void recordOutputs(TaskExecution taskExecution);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2017 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.api.internal.tasks.execution;

import org.gradle.api.file.FileCollection;
import org.gradle.api.internal.TaskInternal;
import org.gradle.api.internal.changedetection.state.TaskOutputFilesRepository;
import org.gradle.api.internal.tasks.TaskExecuter;
import org.gradle.api.internal.tasks.TaskExecutionContext;
import org.gradle.api.internal.tasks.TaskOutputFilePropertySpec;
import org.gradle.api.internal.tasks.TaskStateInternal;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.internal.cleanup.BuildOutputCleanupRegistry;
import org.gradle.internal.operations.BuildOperationContext;
import org.gradle.internal.operations.BuildOperationExecutor;
import org.gradle.internal.operations.RunnableBuildOperation;
import org.gradle.internal.progress.BuildOperationDescriptor;
import org.gradle.util.GFileUtils;

import java.io.File;

public class CleanupStaleOutputsExecuter implements TaskExecuter {

public static final String CLEAN_STALE_OUTPUTS_DISPLAY_NAME = "Clean stale outputs";

private final Logger logger = Logging.getLogger(CleanupStaleOutputsExecuter.class);
private final BuildOperationExecutor buildOperationExecutor;
private final TaskExecuter executer;
private final TaskOutputFilesRepository taskOutputFilesRepository;
private final BuildOutputCleanupRegistry cleanupRegistry;

public CleanupStaleOutputsExecuter(BuildOutputCleanupRegistry cleanupRegistry, TaskOutputFilesRepository taskOutputFilesRepository, BuildOperationExecutor buildOperationExecutor, TaskExecuter executer) {
this.cleanupRegistry = cleanupRegistry;
this.buildOperationExecutor = buildOperationExecutor;
this.executer = executer;
this.taskOutputFilesRepository = taskOutputFilesRepository;
}

@Override
public void execute(final TaskInternal task, TaskStateInternal state, TaskExecutionContext context) {
buildOperationExecutor.run(new RunnableBuildOperation() {
@Override
public void run(BuildOperationContext context) {
for (TaskOutputFilePropertySpec outputFileSpec : task.getOutputs().getFileProperties()) {
FileCollection files = outputFileSpec.getPropertyFiles();
for (File file : files) {
if (cleanupRegistry.isOutputOwnedByBuild(file) && !taskOutputFilesRepository.isGeneratedByGradle(file) && file.exists()) {
logger.info("Deleting stale output file: {}", file.getAbsolutePath());
GFileUtils.forceDelete(file);
}
}
}
}

@Override
public BuildOperationDescriptor.Builder description() {
return BuildOperationDescriptor.displayName(CLEAN_STALE_OUTPUTS_DISPLAY_NAME).progressDisplayName("Cleaning stale outputs");
}
});
executer.execute(task, state, context);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.internal.Cast;
import org.gradle.internal.cleanup.BuildOutputCleanupRegistry;

import java.io.File;
import java.util.Set;
Expand All @@ -38,10 +39,12 @@
public class SkipEmptySourceFilesTaskExecuter implements TaskExecuter {
private static final Logger LOGGER = Logging.getLogger(SkipEmptySourceFilesTaskExecuter.class);
private final TaskInputsListener taskInputsListener;
private final BuildOutputCleanupRegistry buildOutputCleanupRegistry;
private final TaskExecuter executer;

public SkipEmptySourceFilesTaskExecuter(TaskInputsListener taskInputsListener, TaskExecuter executer) {
public SkipEmptySourceFilesTaskExecuter(TaskInputsListener taskInputsListener, BuildOutputCleanupRegistry buildOutputCleanupRegistry, TaskExecuter executer) {
this.taskInputsListener = taskInputsListener;
this.buildOutputCleanupRegistry = buildOutputCleanupRegistry;
this.executer = executer;
}

Expand All @@ -61,7 +64,7 @@ public void execute(TaskInternal task, TaskStateInternal state, TaskExecutionCon
boolean deletedFiles = false;
boolean debugEnabled = LOGGER.isDebugEnabled();
for (File file : outputFileSet) {
if (file.isFile()) {
if (file.isFile() && buildOutputCleanupRegistry.isOutputOwnedByBuild(file)) {
if (file.delete()) {
if (debugEnabled) {
LOGGER.debug("Deleted stale output file '{}'.", file.getAbsolutePath());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import org.gradle.internal.buildevents.TaskExecutionLogger;
import org.gradle.internal.buildevents.TaskExecutionStatisticsReporter;
import org.gradle.internal.classpath.ClassPath;
import org.gradle.internal.cleanup.BuildOutputCleanupListener;
import org.gradle.internal.concurrent.Stoppable;
import org.gradle.internal.event.ListenerManager;
import org.gradle.internal.featurelifecycle.LoggingDeprecatedFeatureHandler;
Expand Down Expand Up @@ -141,8 +140,6 @@ private DefaultGradleLauncher doNewInstance(StartParameter startParameter, Gradl

ScriptUsageLocationReporter usageLocationReporter = new ScriptUsageLocationReporter();
listenerManager.addListener(usageLocationReporter);
BuildOutputCleanupListener buildOutputCleanupListener = serviceRegistry.get(BuildOutputCleanupListener.class);
listenerManager.addListener(buildOutputCleanupListener);
ShowStacktrace showStacktrace = startParameter.getShowStacktrace();
switch (showStacktrace) {
case ALWAYS:
Expand Down

This file was deleted.

Loading

0 comments on commit dbf5680

Please sign in to comment.