Skip to content

Commit 5301289

Browse files
authored
[8.2.1] Fix atomic writes of VirtualActionInputs on Windows (#25857)
Moves to files that are concurrently open for reading fail on Windows, which makes it necessary to ignore this particular mode of failure when atomically writing a `VirtualActionInput`. Fixes #25800 Closes #25805. PiperOrigin-RevId: 746475729 Change-Id: I6830bc6cd8d5d32242b8823f0d5fc4587223c7b5 (cherry picked from commit b44530d) Closes #25851
1 parent a5e1033 commit 5301289

File tree

4 files changed

+122
-17
lines changed

4 files changed

+122
-17
lines changed

src/main/java/com/google/devtools/build/lib/actions/cache/VirtualActionInput.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
import com.google.devtools.build.lib.actions.ActionInput;
1818
import com.google.devtools.build.lib.actions.FileArtifactValue;
1919
import com.google.devtools.build.lib.util.StreamWriter;
20+
import com.google.devtools.build.lib.util.OS;
21+
import com.google.devtools.build.lib.vfs.FileAccessException;
2022
import com.google.devtools.build.lib.vfs.FileSystem;
2123
import com.google.devtools.build.lib.vfs.Path;
2224
import com.google.devtools.build.lib.vfs.PathFragment;
2325
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2426
import com.google.protobuf.ByteString;
2527
import java.io.IOException;
2628
import java.io.OutputStream;
29+
import java.util.Arrays;
2730
import java.util.concurrent.atomic.AtomicInteger;
2831

2932
/**
@@ -49,9 +52,8 @@ public abstract class VirtualActionInput implements ActionInput, StreamWriter {
4952
* by the local and remote branches.
5053
*
5154
* <p>This implementation works by first creating a temporary file with a unique name and then
52-
* renaming it into place, relying on the atomicity of {@link FileSystem#renameTo} (which is
53-
* guaranteed for Unix filesystems, but possibly not for Windows). Subclasses may provide a more
54-
* efficient implementation.
55+
* renaming it into place, relying on the atomicity of {@link FileSystem#renameTo}. Subclasses may
56+
* provide a more efficient implementation.
5557
*
5658
* @param execRoot the path that this input should be written inside, typically the execroot
5759
* @return digest of written virtual input
@@ -80,7 +82,15 @@ protected byte[] atomicallyWriteTo(Path outputPath) throws IOException {
8082
tmpPath.delete();
8183
try {
8284
byte[] digest = writeTo(tmpPath);
83-
tmpPath.renameTo(outputPath);
85+
try {
86+
tmpPath.renameTo(outputPath);
87+
} catch (FileAccessException e) {
88+
// Moves fail on Windows if the target is accessed concurrently.
89+
if (OS.getCurrent() == OS.WINDOWS && Arrays.equals(outputPath.getDigest(), digest)) {
90+
return digest;
91+
}
92+
throw e;
93+
}
8494
tmpPath = null; // Avoid unnecessary deletion attempt.
8595
return digest;
8696
} finally {

src/test/java/com/google/devtools/build/lib/actions/BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,14 @@ java_library(
6767
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils:depsutils",
6868
"//src/main/java/com/google/devtools/build/lib/starlarkbuildapi",
6969
"//src/main/java/com/google/devtools/build/lib/testing/common:directory_listing_helper",
70+
"//src/main/java/com/google/devtools/build/lib/unix",
7071
"//src/main/java/com/google/devtools/build/lib/util",
7172
"//src/main/java/com/google/devtools/build/lib/util:filetype",
73+
"//src/main/java/com/google/devtools/build/lib/util:os",
7274
"//src/main/java/com/google/devtools/build/lib/vfs",
7375
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
7476
"//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
77+
"//src/main/java/com/google/devtools/build/lib/windows",
7578
"//src/main/java/com/google/devtools/build/lib/worker",
7679
"//src/main/java/com/google/devtools/build/lib/worker:worker_key",
7780
"//src/main/java/com/google/devtools/build/lib/worker:worker_process_status",
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2014 The Bazel Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package com.google.devtools.build.lib.actions.cache;
15+
16+
import static com.google.common.truth.Truth.assertThat;
17+
import static com.google.devtools.build.lib.vfs.DigestHashFunction.SHA256;
18+
import static java.nio.charset.StandardCharsets.UTF_8;
19+
20+
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
21+
import com.google.devtools.build.lib.unix.UnixFileSystem;
22+
import com.google.devtools.build.lib.util.OS;
23+
import com.google.devtools.build.lib.vfs.Dirent;
24+
import com.google.devtools.build.lib.vfs.FileSystem;
25+
import com.google.devtools.build.lib.vfs.FileSystemUtils;
26+
import com.google.devtools.build.lib.vfs.JavaIoFileSystem;
27+
import com.google.devtools.build.lib.vfs.Path;
28+
import com.google.devtools.build.lib.vfs.Symlinks;
29+
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
30+
import com.google.devtools.build.lib.windows.WindowsFileSystem;
31+
import com.google.testing.junit.testparameterinjector.TestParameter;
32+
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
33+
import org.junit.Rule;
34+
import org.junit.Test;
35+
import org.junit.rules.TemporaryFolder;
36+
import org.junit.runner.RunWith;
37+
38+
@RunWith(TestParameterInjector.class)
39+
public class VirtualActionInputTest {
40+
@Rule public TemporaryFolder tempFolder = new TemporaryFolder();
41+
42+
public enum FileSystemType {
43+
IN_MEMORY,
44+
JAVA,
45+
NATIVE;
46+
47+
FileSystem getFileSystem() {
48+
return switch (this) {
49+
case IN_MEMORY -> new InMemoryFileSystem(SHA256);
50+
case JAVA -> new JavaIoFileSystem(SHA256);
51+
case NATIVE ->
52+
OS.getCurrent() == OS.WINDOWS
53+
? new WindowsFileSystem(SHA256, /* createSymbolicLinks= */ false)
54+
: new UnixFileSystem(SHA256, "hash");
55+
};
56+
}
57+
}
58+
59+
@Test
60+
public void testAtomicallyWriteRelativeTo(@TestParameter FileSystemType fileSystemType)
61+
throws Exception {
62+
FileSystem fs = fileSystemType.getFileSystem();
63+
Path execRoot = fs.getPath(tempFolder.getRoot().getPath());
64+
65+
Path outputFile = execRoot.getRelative("some/file");
66+
VirtualActionInput input =
67+
ActionsTestUtil.createVirtualActionInput(
68+
outputFile.relativeTo(execRoot).getPathString(), "hello");
69+
70+
var digest = input.atomicallyWriteRelativeTo(execRoot);
71+
72+
assertThat(outputFile.getParentDirectory().readdir(Symlinks.NOFOLLOW))
73+
.containsExactly(new Dirent("file", Dirent.Type.FILE));
74+
assertThat(FileSystemUtils.readLines(outputFile, UTF_8)).containsExactly("hello");
75+
assertThat(outputFile.isExecutable()).isTrue();
76+
assertThat(digest).isEqualTo(SHA256.getHashFunction().hashString("hello", UTF_8).asBytes());
77+
}
78+
79+
@Test
80+
public void testAtomicallyWriteRelativeTo_concurrentRead(
81+
@TestParameter FileSystemType fileSystemType) throws Exception {
82+
FileSystem fs = fileSystemType.getFileSystem();
83+
Path execRoot = fs.getPath(tempFolder.getRoot().getPath());
84+
85+
Path outputFile = execRoot.getRelative("some/file");
86+
VirtualActionInput input =
87+
ActionsTestUtil.createVirtualActionInput(
88+
outputFile.relativeTo(execRoot).getPathString(), "hello");
89+
90+
input.atomicallyWriteRelativeTo(execRoot);
91+
byte[] digest;
92+
byte[] bytes;
93+
try (var in = outputFile.getInputStream()) {
94+
digest = input.atomicallyWriteRelativeTo(execRoot);
95+
bytes = in.readAllBytes();
96+
}
97+
98+
assertThat(outputFile.getParentDirectory().readdir(Symlinks.NOFOLLOW))
99+
.containsExactly(new Dirent("file", Dirent.Type.FILE));
100+
assertThat(FileSystemUtils.readLines(outputFile, UTF_8)).containsExactly("hello");
101+
assertThat(outputFile.isExecutable()).isTrue();
102+
assertThat(digest).isEqualTo(SHA256.getHashFunction().hashString("hello", UTF_8).asBytes());
103+
assertThat(bytes).isEqualTo("hello".getBytes(UTF_8));
104+
}
105+
}

src/test/java/com/google/devtools/build/lib/sandbox/SandboxHelpersTest.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -216,19 +216,6 @@ public void atomicallyWriteVirtualInput_writesBinToolsFile() throws Exception {
216216
assertThat(outputFile.isExecutable()).isTrue();
217217
}
218218

219-
@Test
220-
public void atomicallyWriteVirtualInput_writesArbitraryVirtualInput() throws Exception {
221-
VirtualActionInput input = ActionsTestUtil.createVirtualActionInput("file", "hello");
222-
223-
input.atomicallyWriteRelativeTo(scratch.resolve("/outputs"));
224-
225-
assertThat(scratch.resolve("/outputs").readdir(Symlinks.NOFOLLOW))
226-
.containsExactly(new Dirent("file", Dirent.Type.FILE));
227-
Path outputFile = scratch.resolve("/outputs/file");
228-
assertThat(FileSystemUtils.readLines(outputFile, UTF_8)).containsExactly("hello");
229-
assertThat(outputFile.isExecutable()).isTrue();
230-
}
231-
232219
@Test
233220
public void cleanExisting_updatesDirs() throws IOException, InterruptedException {
234221
Path inputTxt = scratch.getFileSystem().getPath(PathFragment.create("/hello.txt"));

0 commit comments

Comments
 (0)