blob: 6884daa9cc7c25d819219396c367edd2254b0c1f [file] [log] [blame]
// Copyright (C) 2017 The Android Open Source Project
//
// 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 com.googlesource.gerrit.plugins.events.fsstore;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.concurrent.TimeUnit;
public class FsTransaction {
/**
* A class to keep track of scratch pads to safely build proposals, and to safely delete them
* during cleanup.
*
* <p>The first assumption is that unique dirs under the build dir will be used for building, and
* that these may be deleted at any time to keep the filesystem clean under the assumption that
* they may be stale. The contract however, is that all deleting must be done by first moving the
* toplevel dir to the delete directory. This ensures that the processes creating entries under
* the build dir will always have their entries intact or non-existing, but never partially what
* they expect.
*
* <p>The next assumption is that all entries under the build dir are not only safe to delete at
* any time, but that they should be deleted by helping processes to ensure that interrupted
* processes do not lead to entry build up in the filesystem.
*/
public static class BasePaths {
public final Path base;
public final Path build;
public final Path delete;
// Stale entries should be designed to be rare, and only happen during
// unclean shutdowns.
public long cleanInterval = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
public int maxDelete = 5; // keep low to not slowdown current update much.
protected long lastClean;
public BasePaths(Path base) {
this.base = base;
build = base.resolve("build");
delete = base.resolve("delete");
}
public void autoClean() {
if (needsClean()) {
FileTime expiry = Fs.getFileTimeAgo(1, TimeUnit.DAYS);
// maxDelete spreads a large cleaning burden over multiple updates
if (clean(expiry, maxDelete)) {
lastClean = System.currentTimeMillis();
}
}
}
/** Clean up to 'max' expired (presumably stale) entries */
public boolean clean(FileTime expiry, int max) {
try {
return Fs.tryRecursiveDeleteEntriesOlderThan(delete, expiry, max)
|| renameAndDeleteEntriesOlderThan(build, delete, expiry, max);
} catch (IOException e) {
// If we knew if it was a repeat offender, we could consider logging it.
return true; // Don't keep retrying failures.
}
}
protected boolean needsClean() {
return System.currentTimeMillis() - cleanInterval > lastClean;
}
}
/** A tempdirectory builder that gets automatically cleaned up. */
protected static class Builder implements AutoCloseable {
public final BasePaths paths;
public final Path dir;
public Builder(BasePaths paths) throws IOException {
this.paths = paths;
Files.createDirectories(paths.build);
Files.createDirectories(paths.delete);
dir = Files.createTempDirectory(paths.build, null);
}
public void close() throws IOException {
FsTransaction.renameAndDelete(dir, paths.delete);
paths.autoClean();
}
}
/**
* Used to atomically delete a directory tree. Avoids name collisions with other processes
* potentially using the same source name directory. Collisions could prevent the move to the
* delete directory from succeeding.
*/
public static void renameAndDelete(Path src, Path del) throws IOException {
if (Files.exists(src)) {
Path tmp = Files.createTempDirectory(del, null);
Path reparented = Fs.reparent(src, tmp);
Fs.tryAtomicMove(src, reparented);
Fs.tryRecursiveDelete(tmp);
}
}
/**
* Used to atomically delete a directory tree when the src directory name is guaranteed to be
* unique.
*/
public static void renameAndDeleteUnique(Path src, Path del) throws IOException {
Path reparented = Fs.reparent(src, del);
Fs.tryAtomicMove(src, reparented);
Fs.tryRecursiveDelete(reparented);
}
/**
* Used to atomically delete entries in a directory tree older than expiry, up to max count. Do
* NOT throw IOExceptions.
*
* @return whether all entries were deleted
*/
public static boolean renameAndDeleteEntriesOlderThan(
Path dir, Path del, FileTime expiry, int max) throws IOException {
try (DirectoryStream<Path> dirEntries = Files.newDirectoryStream(dir)) {
for (Path path : dirEntries) {
if (expiry.compareTo(Files.getLastModifiedTime(path)) > 0) {
if (max-- < 1) {
return false;
}
renameAndDelete(path, del);
}
}
return true;
}
}
}