Add a new `FileSystem` method to get a `SeekableByteChannel` for writing files.
Add `FileSystem::createSeekableChannel` which allows us to create a `SeekableByteChannel` for file writes. The channel is opened in truncated mode, meaning that it is not suitable for reading existing files.
PiperOrigin-RevId: 393833983
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
index 8a4e53d..1ff1435 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
@@ -14,8 +14,13 @@
//
package com.google.devtools.build.lib.vfs;
+import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.READ;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
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;
@@ -27,8 +32,11 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
-import java.util.EnumSet;
+import java.nio.file.OpenOption;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
/** This class implements the FileSystem interface using direct calls to the UNIX filesystem. */
@ThreadSafe
@@ -83,21 +91,42 @@
}
}
+ private static final ImmutableSet<StandardOpenOption> READABLE_BYTE_CHANNEL_OPEN_OPTIONS =
+ Sets.immutableEnumSet(READ);
+ private static final ImmutableSet<ProfilerTask> READABLE_BYTE_CHANNEL_PROFILER_TASKS =
+ Sets.immutableEnumSet(ProfilerTask.VFS_OPEN, ProfilerTask.VFS_READ);
+ private static final ImmutableSet<StandardOpenOption> READ_WRITE_BYTE_CHANNEL_OPEN_OPTIONS =
+ Sets.immutableEnumSet(READ, WRITE, CREATE, TRUNCATE_EXISTING);
+ private static final ImmutableSet<ProfilerTask> READ_WRITE_BYTE_CHANNEL_PROFILER_TASKS =
+ Sets.immutableEnumSet(ProfilerTask.VFS_OPEN, ProfilerTask.VFS_READ, ProfilerTask.VFS_WRITE);
+
@Override
protected ReadableByteChannel createReadableByteChannel(PathFragment path) throws IOException {
- final String name = path.toString();
- if (profiler.isActive()
- && (profiler.isProfiling(ProfilerTask.VFS_READ)
- || profiler.isProfiling(ProfilerTask.VFS_OPEN))) {
- long startTime = Profiler.nanoTimeMaybe();
- try {
- // Currently, we do not proxy ReadableByteChannel for profiling.
- return Files.newByteChannel(java.nio.file.Paths.get(name), EnumSet.of(READ));
- } finally {
- profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, name);
- }
- } else {
- return Files.newByteChannel(java.nio.file.Paths.get(name));
+ return createSeekableByteChannelInternal(
+ path, READABLE_BYTE_CHANNEL_OPEN_OPTIONS, READABLE_BYTE_CHANNEL_PROFILER_TASKS);
+ }
+
+ @Override
+ protected SeekableByteChannel createReadWriteByteChannel(PathFragment path) throws IOException {
+ return createSeekableByteChannelInternal(
+ path, READ_WRITE_BYTE_CHANNEL_OPEN_OPTIONS, READ_WRITE_BYTE_CHANNEL_PROFILER_TASKS);
+ }
+
+ private static SeekableByteChannel createSeekableByteChannelInternal(
+ PathFragment path,
+ ImmutableSet<? extends OpenOption> options,
+ ImmutableSet<ProfilerTask> profilerTasks)
+ throws IOException {
+ String name = path.getPathString();
+ if (!profiler.isActive() || profilerTasks.stream().noneMatch(profiler::isProfiling)) {
+ return Files.newByteChannel(Paths.get(name), options);
+ }
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ // Currently, we do not proxy SeekableByteChannel for profiling.
+ return Files.newByteChannel(Paths.get(name), options);
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, name);
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/BUILD b/src/main/java/com/google/devtools/build/lib/vfs/BUILD
index bcd1791..2f1c648 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/vfs/BUILD
@@ -59,6 +59,7 @@
"//third_party:caffeine",
"//third_party:guava",
"//third_party:jsr305",
+ "//third_party/protobuf:protobuf_java",
],
)
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
index 283e87b..67a225b 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
@@ -28,6 +28,7 @@
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileAlreadyExistsException;
import java.util.Collection;
import java.util.Iterator;
@@ -720,6 +721,16 @@
}
/**
+ * Returns a {@link SeekableByteChannel} for writing to a file at provided path.
+ *
+ * <p>Truncates the target file, therefore it cannot be used to read already existing files.
+ * Please use {@link #createReadableByteChannel} to get a {@linkplain ReadableByteChannel channel}
+ * for reads instead.
+ */
+ protected abstract SeekableByteChannel createReadWriteByteChannel(PathFragment path)
+ throws IOException;
+
+ /**
* Creates an OutputStream accessing the file denoted by path.
*
* @throws IOException if there was an error opening the file for writing
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/Path.java b/src/main/java/com/google/devtools/build/lib/vfs/Path.java
index 71d21c6..bfc4161 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/Path.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/Path.java
@@ -32,6 +32,7 @@
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
import java.util.ArrayList;
import java.util.Collection;
import javax.annotation.Nullable;
@@ -790,10 +791,21 @@
}
/**
+ * Opens the file denoted by this path, following symbolic links, for reading and writing and
+ * returns a file channel for it.
+ *
+ * <p>Truncates the file, therefore it cannot be used to read already existing files. Please use
+ * {@link #createReadableByteChannel} to get a {@linkplain ReadableByteChannel channel} for reads
+ * instead.
+ */
+ public SeekableByteChannel createReadWriteByteChannel() throws IOException {
+ return fileSystem.createReadWriteByteChannel(asFragment());
+ }
+
+ /**
* Returns a java.io.File representation of this path.
*
- * <p>Caveat: the result may be useless if this path's getFileSystem() is not
- * the UNIX filesystem.
+ * <p>Caveat: the result may be useless if this path's getFileSystem() is not the UNIX filesystem.
*/
public File getPathFile() {
return new File(getPathString());
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathTransformingDelegateFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/PathTransformingDelegateFileSystem.java
index 3070239..063ecc6 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/PathTransformingDelegateFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/PathTransformingDelegateFileSystem.java
@@ -19,6 +19,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
import java.util.Collection;
/**
@@ -178,6 +179,11 @@
}
@Override
+ protected SeekableByteChannel createReadWriteByteChannel(PathFragment path) throws IOException {
+ return delegateFs.createReadWriteByteChannel(toDelegatePath(path));
+ }
+
+ @Override
protected OutputStream getOutputStream(PathFragment path, boolean append) throws IOException {
return delegateFs.getOutputStream(toDelegatePath(path), append);
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
index 097f7fa..39eb14f 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
@@ -35,6 +35,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
@@ -611,6 +612,12 @@
}
@Override
+ protected synchronized SeekableByteChannel createReadWriteByteChannel(PathFragment path) {
+ // It's feasible to implement, but so far it is not needed.
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
protected synchronized byte[] getFastDigest(PathFragment path) throws IOException {
return statFile(path).getFastDigest();
}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/BUILD b/src/test/java/com/google/devtools/build/lib/vfs/BUILD
index 49defcd..662b976 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/vfs/BUILD
@@ -66,10 +66,8 @@
"//src/test/java/com/google/devtools/build/lib/testutil:TestThread",
"//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
"//src/test/java/com/google/devtools/build/lib/vfs/util",
- "//third_party:caffeine",
"//third_party:guava",
"//third_party:guava-testlib",
- "//third_party:jsr305",
"//third_party:junit4",
"//third_party:mockito",
"//third_party:truth",
@@ -90,6 +88,7 @@
"//third_party:guava",
"//third_party:junit4",
"//third_party:truth",
+ "@com_google_testparameterinjector//:testparameterinjector",
],
)
@@ -147,5 +146,6 @@
"//third_party:guava",
"//third_party:junit4",
"//third_party:truth",
+ "@com_google_testparameterinjector//:testparameterinjector",
],
)
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
index b4f6f60..09fc304 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
@@ -14,7 +14,9 @@
//
package com.google.devtools.build.lib.vfs;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
@@ -26,6 +28,9 @@
import com.google.devtools.build.lib.unix.FileStatus;
import com.google.devtools.build.lib.unix.NativePosixFiles;
import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -33,9 +38,10 @@
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
-import java.util.Collection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
@@ -44,17 +50,13 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
-import org.junit.runners.Parameterized.Parameters;
/**
* This class handles the generic tests that any filesystem must pass.
*
- * <p>Each filesystem-test should inherit from this class, thereby obtaining
- * all the tests.
+ * <p>Each filesystem-test should inherit from this class, thereby obtaining all the tests.
*/
-@RunWith(Parameterized.class)
+@RunWith(TestParameterInjector.class)
public abstract class FileSystemTest {
private long savedTime;
@@ -69,18 +71,15 @@
protected Path xNonEmptyDirectoryFoo;
protected Path xEmptyDirectory;
- @Parameters(name = "{index}: digestHashFunction={0}")
- public static Collection<DigestHashFunction[]> hashFunctions() {
- // TODO(b/112537387): Remove the array-ification and return Collection<DigestHashFunction>. This
- // is possible in Junit4.12, but 4.11 requires the array. Bazel 0.18 will have Junit4.12, so
- // this can change then.
- return DigestHashFunction.getPossibleHashFunctions()
- .stream()
- .map(dhf -> new DigestHashFunction[] {dhf})
- .collect(ImmutableList.toImmutableList());
- }
+ @TestParameter(valuesProvider = DigestHashFunctionsProvider.class)
+ public DigestHashFunction digestHashFunction;
- @Parameter public DigestHashFunction digestHashFunction;
+ private static final class DigestHashFunctionsProvider implements TestParameterValuesProvider {
+ @Override
+ public ImmutableList<?> provideValues() {
+ return DigestHashFunction.getPossibleHashFunctions().asList();
+ }
+ }
@Before
public final void createDirectories() throws Exception {
@@ -1321,6 +1320,74 @@
}
}
+ @Test
+ public void testCreateReadWriteByteChannelWrite(@TestParameter boolean overwrite)
+ throws Exception {
+ String text = "hello";
+ Path file = overwrite ? xFile : xNothing;
+ try (SeekableByteChannel channel = file.createReadWriteByteChannel()) {
+ writeToChannelAsLatin1(channel, text);
+ assertThat(channel.position()).isEqualTo(text.length());
+ }
+
+ assertThat(FileSystemUtils.readContent(file, ISO_8859_1)).isEqualTo("hello");
+ }
+
+ @Test
+ public void testCreateReadWriteByteChannelWriteAfterSeek() throws Exception {
+ try (SeekableByteChannel channel = xNothing.createReadWriteByteChannel()) {
+ writeToChannelAsLatin1(channel, "01234567890");
+ channel.position(5);
+ writeToChannelAsLatin1(channel, "hello!");
+ assertThat(channel.position()).isEqualTo(5 + "hello!".length());
+ }
+
+ assertThat(FileSystemUtils.readContent(xNothing, ISO_8859_1)).isEqualTo("01234hello!");
+ }
+
+ @Test
+ public void testCreateReadWriteByteChannelRead(@TestParameter({"0", "5", "12"}) int seekPosition)
+ throws Exception {
+ String text = "hello there!";
+ try (SeekableByteChannel channel = xNothing.createReadWriteByteChannel()) {
+ writeToChannelAsLatin1(channel, text);
+ channel.position(seekPosition);
+
+ String read = readAllAsString(channel, text.length() - seekPosition);
+ assertThat(channel.position()).isEqualTo(text.length());
+ assertThat(read).isEqualTo(text.substring(seekPosition));
+ }
+ }
+
+ private static void writeToChannelAsLatin1(WritableByteChannel channel, String text)
+ throws IOException {
+ byte[] bytes = text.getBytes(ISO_8859_1);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes);
+ int toWrite = bytes.length;
+ while (toWrite > 0) {
+ toWrite -= channel.write(buffer);
+ }
+ assertThat(toWrite).isEqualTo(0);
+ assertThat(buffer.remaining()).isEqualTo(0);
+ }
+
+ private static String readAllAsString(ReadableByteChannel channel, int expectedSize)
+ throws IOException {
+ checkArgument(expectedSize >= 0, "negative expected size: %s", expectedSize);
+ // +1 to make sure we can observe EOF -- Channel::read will always return 0 for a full buffer.
+ ByteBuffer buffer = ByteBuffer.allocate(expectedSize + 1);
+ int totalRead = 0;
+ for (; ; ) {
+ int read = channel.read(buffer);
+ if (read == -1) {
+ assertThat(totalRead).isEqualTo(expectedSize);
+ return new String(buffer.array(), 0, expectedSize, ISO_8859_1);
+ }
+ totalRead += read;
+ assertThat(buffer.position()).isEqualTo(totalRead);
+ }
+ }
+
private static int readFromChannel(ReadableByteChannel channel, ByteBuffer buffer, int expected)
throws IOException {
int numRead = 0;
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystemTest.java
index e1654a0..827e3f2 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystemTest.java
@@ -26,6 +26,7 @@
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.SymlinkAwareFileSystemTest;
+import com.google.testing.junit.testparameterinjector.TestParameter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -64,8 +65,8 @@
}
/**
- * Tests concurrent creation of a substantial tree hierarchy including
- * files, directories, symlinks, file contents, and permissions.
+ * Tests concurrent creation of a substantial tree hierarchy including files, directories,
+ * symlinks, file contents, and permissions.
*/
@Test
public void testConcurrentTreeConstruction() throws Exception {
@@ -410,4 +411,19 @@
assertThat(symlink.getxattr("some.xattr")).isNull();
}
+
+ // createSeekableByteChannel is not supported by InMemoryFileSystem.
+ @Test
+ @Override
+ public void testCreateReadWriteByteChannelWrite(@TestParameter boolean overwrite) {}
+
+ // createSeekableByteChannel is not supported by InMemoryFileSystem.
+ @Test
+ @Override
+ public void testCreateReadWriteByteChannelWriteAfterSeek() {}
+
+ // createSeekableByteChannel is not supported by InMemoryFileSystem.
+ @Test
+ @Override
+ public void testCreateReadWriteByteChannelRead(@TestParameter({"0"}) int seekPosition) {}
}