blob: 1f6006d7d156b74fa9ea8a4063103056f325bc9e [file] [log] [blame]
// Copyright 2019 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.vfs;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
import com.google.devtools.build.lib.util.FileChannels;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
/**
* This class extends {@link FileSystem} with default implementations providing access to files on
* disk through standard library APIs.
*/
@ThreadSafe
public abstract class DiskBackedFileSystem extends FileSystem {
private static final Profiler profiler = Profiler.instance();
private static final ImmutableSet<StandardOpenOption> READ_WRITE_BYTE_CHANNEL_OPEN_OPTIONS =
ImmutableSet.of(
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
protected DiskBackedFileSystem(DigestHashFunction hashFunction) {
super(hashFunction);
}
// Force subclasses to override getIoFile and getNioPath, as the methods below require them.
@Override
public abstract File getIoFile(PathFragment path);
@Override
public abstract java.nio.file.Path getNioPath(PathFragment path);
@Override
public InputStream getInputStream(PathFragment path) throws IOException {
File file = checkNotNull(getIoFile(path), "getIoFile() must not be null");
boolean profileOpen = profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_OPEN);
boolean profileRead = profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_READ);
long startTime = profiler.nanoTimeMaybe();
try {
return profileRead
? new ProfiledPatchedFileInputStream(file, path.getPathString())
: new PatchedFileInputStream(file);
} catch (FileNotFoundException e) {
// FileInputStream throws FileNotFoundException if opening fails for any reason, including
// permissions. Fix it up here.
if (e.getMessage().endsWith(ERR_PERMISSION_DENIED)) {
throw new FileAccessException(e.getMessage());
}
throw e;
} finally {
if (profileOpen) {
profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, path.getPathString());
}
}
}
@Override
public OutputStream getOutputStream(PathFragment path, boolean append, boolean internal)
throws IOException {
File file = checkNotNull(getIoFile(path), "getIoFile() must not be null");
boolean profileOpen =
!internal && profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_OPEN);
boolean profileWrite =
!internal && profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_WRITE);
long startTime = profiler.nanoTimeMaybe();
try {
return profileWrite
? new ProfiledPatchedFileOutputStream(file, append, path.getPathString())
: new PatchedFileOutputStream(file, append);
} catch (FileNotFoundException e) {
// FileOutputStream throws FileNotFoundException if opening fails for any reason, including
// permissions. Fix it up here.
if (e.getMessage().endsWith(ERR_PERMISSION_DENIED)) {
throw new FileAccessException(e.getMessage());
}
throw e;
} finally {
if (profileOpen) {
profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, path.getPathString());
}
}
}
@Override
public SeekableByteChannel createReadWriteByteChannel(PathFragment path) throws IOException {
java.nio.file.Path nioPath = checkNotNull(getNioPath(path), "getNioPath() must not be null");
boolean profileOpen = profiler.isActive() && profiler.isProfiling(ProfilerTask.VFS_OPEN);
long startTime = Profiler.instance().nanoTimeMaybe();
try {
// TODO: add profiling for read/write operations.
return Files.newByteChannel(nioPath, READ_WRITE_BYTE_CHANNEL_OPEN_OPTIONS);
} finally {
if (profileOpen) {
profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, path.toString());
}
}
}
// As of OpenJDK 25, FileInputStream.transferTo(FileOutputStream) closes the underlying
// FileChannels and throws a ClosedByInterruptException (a subclass of IOException) when called
// with the interrupt bit set. This is unfortunate because it's easy to forget to account for this
// case and interrupt code paths aren't comprehensively tested, making it highly likely that such
// an oversight will result in spurious build failures in production (b/463596620 is an example).
//
// To work around this, we patch FileInputStream/FileOutputStream's getChannel() method so that
// it calls the FileChannelImpl.setUninterruptible() internal OpenJDK API to suppress the
// close-on-interrupt behavior. Interestingly, Files.newInputStream() and Files.newOutputStream()
// already do this, suggesting that FileInputStream and FileOutputStream not doing so might be an
// implementation oversight rather than a deliberate design decision.
//
// This will not work on non-OpenJDK-based JDKs that don't provide this API, but we consider it
// acceptable for the time being.
//
// The following alternatives were considered and rejected:
//
// - Clear the interrupt bit before calling transferTo() and restore it after: does not work,
// as there's still a time window during which the interrupt bit may be set.
//
// - Implement a retry loop around transferTo(): does not work, because a failed first attempt
// closes the FileChannel, thereby ensuring that subsequent attempts will also fail.
//
// - Implement a retry loop around transferTo() while wrapping the FileChannels in a proxy that
// replaces close() with a no-op: while this does appear to work and doesn't rely on internal
// APIs, it's significantly more complex and still relies on implementation details, arguably
// in a more dangerous way. For example, if the FileChannel implementation is such that
// ClosedByInterruptException may be thrown after some bytes have already been transferred,
// the retry loop might accidentally transfer them twice.
//
// - Use Files.newInputStream() and Files.newOutputStream(): not an option, because they return a
// ChannelInputStream or ChannelOutputStream, respectively, and some callers expect a
// FileInputStream or FileOutputStream (in order to be able to call fsync(), for example).
//
// - Provide an interruptibleTransferTo() helper method that converts ClosedByInterruptException
// into InterruptedException and adjust callsites accordingly: undesirable, because it remains
// possible to erroneously call FileInputStream.transferTo(), which is unlikely to be detected
// in testing.
/**
* A {@link FileInputStream} that patches the bug described above.
*
* <p>Implementation note: this class extends {@link FileInputStream} instead of wrapping around
* it so that {@code instanceof FileInputStream} checks still work.
*/
private static class PatchedFileInputStream extends FileInputStream {
private volatile boolean patched = false;
private PatchedFileInputStream(File file) throws IOException {
super(file);
}
@Override
public FileChannel getChannel() {
FileChannel channel = super.getChannel();
// Benign data race: at worst we call setUninterruptible more than once.
if (!patched) {
FileChannels.setUninterruptible(channel);
patched = true;
}
return channel;
}
}
/**
* A {@link FileInputStream} that patches the bug described above and adds profile traces around
* read operations.
*
* <p>Implementation note: this class extends {@link FileInputStream} instead of wrapping around
* it so that {@code instanceof FileInputStream} checks still work.
*/
private static class ProfiledPatchedFileInputStream extends PatchedFileInputStream {
private final String name;
private ProfiledPatchedFileInputStream(File file, String name) throws IOException {
super(file);
this.name = name;
}
@Override
public int read() throws IOException {
long startTime = profiler.nanoTimeMaybe();
try {
return super.read();
} finally {
profiler.logSimpleTask(startTime, ProfilerTask.VFS_READ, name);
}
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
long startTime = profiler.nanoTimeMaybe();
try {
return super.read(b, off, len);
} finally {
profiler.logSimpleTask(startTime, ProfilerTask.VFS_READ, name);
}
}
}
/**
* A {@link FileOutputStream} that patches the bug described above.
*
* <p>Implementation note: this class extends {@link FileOutputStream} instead of wrapping around
* it so that {@code instanceof FileOutputStream} checks still work.
*/
private static class PatchedFileOutputStream extends FileOutputStream {
private volatile boolean patched = false;
private PatchedFileOutputStream(File file, boolean append) throws IOException {
super(file, append);
}
@Override
public FileChannel getChannel() {
FileChannel channel = super.getChannel();
// Benign data race: at worst we call setUninterruptible more than once.
if (!patched) {
FileChannels.setUninterruptible(channel);
patched = true;
}
return channel;
}
}
/**
* A {@link FileOutputStream} that patches the bug described above and adds profile traces around
* write operations.
*
* <p>Implementation note: this class extends {@link FileOutputStream} instead of wrapping around
* it so that {@code instanceof FileOutputStream} checks still work.
*/
private static class ProfiledPatchedFileOutputStream extends PatchedFileOutputStream {
private final String name;
private ProfiledPatchedFileOutputStream(File file, boolean append, String name)
throws IOException {
super(file, append);
this.name = name;
}
@Override
public void write(int b) throws IOException {
long startTime = profiler.nanoTimeMaybe();
try {
super.write(b);
} finally {
profiler.logSimpleTask(startTime, ProfilerTask.VFS_WRITE, name);
}
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
long startTime = profiler.nanoTimeMaybe();
try {
super.write(b, off, len);
} finally {
profiler.logSimpleTask(startTime, ProfilerTask.VFS_WRITE, name);
}
}
}
}