Skip to content

Commit b52c999

Browse files
tjgqShreeM01
andauthored
[6.2.0] Fix data race in prefetcher. (bazelbuild#17744)
Individual file prefetches within a single prefetchFiles() can race against each other, so they must synchronize when writing to the DirectoryContext. Closes bazelbuild#17678. PiperOrigin-RevId: 515024483 Change-Id: Ic8097979d06ab143b4d63f5e90f871f8cbf83959 Co-authored-by: kshyanashree <[email protected]>
1 parent 718bea2 commit b52c999

File tree

4 files changed

+140
-45
lines changed

4 files changed

+140
-45
lines changed

src/main/java/com/google/devtools/build/lib/remote/AbstractActionInputPrefetcher.java

+105-40
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
import java.util.ArrayList;
6161
import java.util.Arrays;
6262
import java.util.Deque;
63-
import java.util.HashSet;
6463
import java.util.List;
6564
import java.util.Set;
6665
import java.util.concurrent.ConcurrentHashMap;
@@ -87,53 +86,124 @@ public abstract class AbstractActionInputPrefetcher implements ActionInputPrefet
8786

8887
private final Set<ActionInput> missingActionInputs = Sets.newConcurrentHashSet();
8988

90-
// Tracks the number of ongoing prefetcher calls temporarily making an output directory writable.
91-
// Since concurrent calls may write to the same directory, it's not safe to make it non-writable
92-
// until no other ongoing calls are writing to it.
93-
private final ConcurrentHashMap<Path, Integer> temporarilyWritableDirectories =
89+
private static final Object dummyValue = new Object();
90+
91+
/**
92+
* Tracks output directories temporarily made writable for prefetching. Since concurrent calls may
93+
* write to the same directory, it's not safe to make it non-writable until no other ongoing
94+
* prefetcher calls are writing to it.
95+
*/
96+
private final ConcurrentHashMap<Path, DirectoryState> temporarilyWritableDirectories =
9497
new ConcurrentHashMap<>();
9598

96-
/** Keeps track of output directories written to by a single prefetcher call. */
99+
/** The state of a single temporarily writable directory. */
100+
private static final class DirectoryState {
101+
/** The number of ongoing prefetcher calls touching this directory. */
102+
int numCalls;
103+
/** Whether the output permissions must be set on the directory when prefetching completes. */
104+
boolean mustSetOutputPermissions;
105+
}
106+
107+
/**
108+
* Tracks output directories written to by a single prefetcher call.
109+
*
110+
* <p>This makes it possible to set the output permissions on directories touched by the
111+
* prefetcher call all at once, so that files prefetched within the same call don't repeatedly set
112+
* output permissions on the same directory.
113+
*/
97114
private final class DirectoryContext {
98-
private final HashSet<Path> dirs = new HashSet<>();
115+
private final ConcurrentHashMap<Path, Object> dirs = new ConcurrentHashMap<>();
99116

100117
/**
101-
* Adds to the set of directories written to by the prefetcher call associated with this
102-
* context.
118+
* Makes a directory temporarily writable for the remainder of the prefetcher call associated
119+
* with this context.
120+
*
121+
* @param isDefinitelyTreeDir Whether this directory definitely belongs to a tree artifact.
122+
* Otherwise, whether it belongs to a tree artifact is inferred from its permissions.
103123
*/
104-
void add(Path dir) {
105-
if (dirs.add(dir)) {
106-
temporarilyWritableDirectories.compute(dir, (unused, count) -> count != null ? ++count : 1);
124+
void createOrSetWritable(Path dir, boolean isDefinitelyTreeDir) throws IOException {
125+
AtomicReference<IOException> caughtException = new AtomicReference<>();
126+
127+
dirs.compute(
128+
dir,
129+
(outerUnused, previousValue) -> {
130+
if (previousValue != null) {
131+
return previousValue;
132+
}
133+
134+
temporarilyWritableDirectories.compute(
135+
dir,
136+
(innerUnused, state) -> {
137+
if (state == null) {
138+
state = new DirectoryState();
139+
state.numCalls = 0;
140+
141+
try {
142+
if (isDefinitelyTreeDir) {
143+
state.mustSetOutputPermissions = true;
144+
var ignored = dir.createWritableDirectory();
145+
} else {
146+
// If the directory is writable, it's a package and should be kept writable.
147+
// Otherwise, it must belong to a tree artifact, since the directory for a
148+
// tree is created in a non-writable state before prefetching begins, and
149+
// this is the first time the prefetcher is seeing it.
150+
state.mustSetOutputPermissions = !dir.isWritable();
151+
if (state.mustSetOutputPermissions) {
152+
dir.setWritable(true);
153+
}
154+
}
155+
} catch (IOException e) {
156+
caughtException.set(e);
157+
return null;
158+
}
159+
}
160+
161+
++state.numCalls;
162+
163+
return state;
164+
});
165+
166+
if (caughtException.get() != null) {
167+
return null;
168+
}
169+
170+
return dummyValue;
171+
});
172+
173+
if (caughtException.get() != null) {
174+
throw caughtException.get();
107175
}
108176
}
109177

110178
/**
111179
* Signals that the prefetcher call associated with this context has finished.
112180
*
113-
* <p>The output permissions will be set on any directories written to by this call that are not
114-
* being written to by other concurrent calls.
181+
* <p>The output permissions will be set on any directories temporarily made writable by this
182+
* call, if this is the last remaining call temporarily making them writable.
115183
*/
116184
void close() throws IOException {
117185
AtomicReference<IOException> caughtException = new AtomicReference<>();
118186

119-
for (Path dir : dirs) {
187+
for (Path dir : dirs.keySet()) {
120188
temporarilyWritableDirectories.compute(
121189
dir,
122-
(unused, count) -> {
123-
checkState(count != null);
124-
if (--count == 0) {
125-
try {
126-
dir.chmod(outputPermissions.getPermissionsMode());
127-
} catch (IOException e) {
128-
// Store caught exceptions, but keep cleaning up the map.
129-
if (caughtException.get() == null) {
130-
caughtException.set(e);
131-
} else {
132-
caughtException.get().addSuppressed(e);
190+
(unused, state) -> {
191+
checkState(state != null);
192+
if (--state.numCalls == 0) {
193+
if (state.mustSetOutputPermissions) {
194+
try {
195+
dir.chmod(outputPermissions.getPermissionsMode());
196+
} catch (IOException e) {
197+
// Store caught exceptions, but keep cleaning up the map.
198+
if (caughtException.get() == null) {
199+
caughtException.set(e);
200+
} else {
201+
caughtException.get().addSuppressed(e);
202+
}
133203
}
134204
}
135205
}
136-
return count > 0 ? count : null;
206+
return state.numCalls > 0 ? state : null;
137207
});
138208
}
139209
dirs.clear();
@@ -518,24 +588,19 @@ private void finalizeDownload(
518588
}
519589
while (!dirs.isEmpty()) {
520590
Path dir = dirs.pop();
521-
dirCtx.add(dir);
522591
// Create directory or make existing directory writable.
523-
var unused = dir.createWritableDirectory();
592+
// We know with certainty that the directory belongs to a tree artifact.
593+
dirCtx.createOrSetWritable(dir, /* isDefinitelyTreeDir= */ true);
524594
}
525595
} else {
526-
// If the parent directory is not writable, temporarily make it so.
527-
// This is needed when fetching a non-tree artifact nested inside a tree artifact, or a tree
528-
// artifact inside a fileset (see b/254844173 for the latter).
529-
// TODO(tjgq): Fix the TOCTTOU race between isWritable and setWritable. This requires keeping
530-
// track of the original directory permissions. Note that nested artifacts are relatively rare
531-
// and will eventually be disallowed (see issue #16729).
532-
if (!parentDir.isWritable()) {
533-
dirCtx.add(parentDir);
534-
parentDir.setWritable(true);
535-
}
596+
// Temporarily make the parent directory writable if needed.
597+
// We don't know with certainty that the directory does not belong to a tree artifact; it
598+
// could if the fetched file is a non-tree artifact nested inside a tree artifact, or a
599+
// tree artifact inside a fileset (see b/254844173 for the latter).
600+
dirCtx.createOrSetWritable(parentDir, /* isDefinitelyTreeDir= */ false);
536601
}
537602

538-
// Set output permissions on files (tree subdirectories are handled in stopPrefetching),
603+
// Set output permissions on files (tree subdirectories are handled in DirectoryContext#close),
539604
// matching the behavior of SkyframeActionExecutor#checkOutputs for artifacts produced by local
540605
// actions.
541606
tmpPath.chmod(outputPermissions.getPermissionsMode());

src/main/java/com/google/devtools/build/lib/testing/vfs/SpiedFileSystem.java

+5
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,9 @@ public static SpiedFileSystem createInMemorySpy() {
5050
public OutputStream getOutputStream(PathFragment path, boolean append) throws IOException {
5151
return super.getOutputStream(path, append);
5252
}
53+
54+
@Override
55+
public boolean createWritableDirectory(PathFragment path) throws IOException {
56+
return super.createWritableDirectory(path);
57+
}
5358
}

src/test/java/com/google/devtools/build/lib/remote/ActionInputPrefetcherTestBase.java

+29-5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static org.mockito.Mockito.doAnswer;
2626
import static org.mockito.Mockito.never;
2727
import static org.mockito.Mockito.spy;
28+
import static org.mockito.Mockito.times;
2829
import static org.mockito.Mockito.verify;
2930

3031
import com.google.common.collect.ImmutableList;
@@ -44,19 +45,17 @@
4445
import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
4546
import com.google.devtools.build.lib.actions.MetadataProvider;
4647
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
47-
import com.google.devtools.build.lib.clock.JavaClock;
4848
import com.google.devtools.build.lib.remote.util.StaticMetadataProvider;
4949
import com.google.devtools.build.lib.remote.util.TempPathGenerator;
5050
import com.google.devtools.build.lib.skyframe.TreeArtifactValue;
51+
import com.google.devtools.build.lib.testing.vfs.SpiedFileSystem;
5152
import com.google.devtools.build.lib.util.Pair;
5253
import com.google.devtools.build.lib.vfs.DigestHashFunction;
5354
import com.google.devtools.build.lib.vfs.Dirent;
54-
import com.google.devtools.build.lib.vfs.FileSystem;
5555
import com.google.devtools.build.lib.vfs.FileSystemUtils;
5656
import com.google.devtools.build.lib.vfs.Path;
5757
import com.google.devtools.build.lib.vfs.PathFragment;
5858
import com.google.devtools.build.lib.vfs.Symlinks;
59-
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
6059
import java.io.IOException;
6160
import java.util.HashMap;
6261
import java.util.Map;
@@ -72,14 +71,14 @@
7271
public abstract class ActionInputPrefetcherTestBase {
7372
protected static final DigestHashFunction HASH_FUNCTION = DigestHashFunction.SHA256;
7473

75-
protected FileSystem fs;
74+
protected SpiedFileSystem fs;
7675
protected Path execRoot;
7776
protected ArtifactRoot artifactRoot;
7877
protected TempPathGenerator tempPathGenerator;
7978

8079
@Before
8180
public void setUp() throws IOException {
82-
fs = new InMemoryFileSystem(new JavaClock(), HASH_FUNCTION);
81+
fs = SpiedFileSystem.createInMemorySpy();
8382
execRoot = fs.getPath("/exec");
8483
execRoot.createDirectoryAndParents();
8584
artifactRoot = ArtifactRoot.asDerivedRoot(execRoot, RootType.Output, "root");
@@ -421,6 +420,31 @@ public void prefetchFiles_ignoreNonRemoteFiles_tree() throws Exception {
421420
assertThat(prefetcher.downloadedFiles()).containsExactly(secondChild.getPath());
422421
}
423422

423+
@Test
424+
public void prefetchFiles_treeFiles_minimizeFilesystemOperations() throws Exception {
425+
Map<ActionInput, FileArtifactValue> metadata = new HashMap<>();
426+
Map<HashCode, byte[]> cas = new HashMap<>();
427+
Pair<SpecialArtifact, ImmutableList<TreeFileArtifact>> treeAndChildren =
428+
createRemoteTreeArtifact(
429+
"dir",
430+
/* localContentMap= */ ImmutableMap.of("subdir/file1", "content1"),
431+
/* remoteContentMap= */ ImmutableMap.of("subdir/file2", "content2"),
432+
metadata,
433+
cas);
434+
SpecialArtifact tree = treeAndChildren.getFirst();
435+
ImmutableList<TreeFileArtifact> children = treeAndChildren.getSecond();
436+
Artifact firstChild = children.get(0);
437+
Artifact secondChild = children.get(1);
438+
439+
MetadataProvider metadataProvider = new StaticMetadataProvider(metadata);
440+
AbstractActionInputPrefetcher prefetcher = createPrefetcher(cas);
441+
442+
wait(prefetcher.prefetchFiles(ImmutableList.of(firstChild, secondChild), metadataProvider));
443+
444+
verify(fs, times(1)).createWritableDirectory(tree.getPath().asFragment());
445+
verify(fs, times(1)).createWritableDirectory(tree.getPath().getChild("subdir").asFragment());
446+
}
447+
424448
@Test
425449
public void prefetchFiles_multipleThreads_downloadIsCancelled() throws Exception {
426450
// Test shared downloads are cancelled if all threads/callers are interrupted

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

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ java_test(
8484
"//src/main/java/com/google/devtools/build/lib/remote/util",
8585
"//src/main/java/com/google/devtools/build/lib/runtime/commands",
8686
"//src/main/java/com/google/devtools/build/lib/skyframe:tree_artifact_value",
87+
"//src/main/java/com/google/devtools/build/lib/testing/vfs:spied_filesystem",
8788
"//src/main/java/com/google/devtools/build/lib/util",
8889
"//src/main/java/com/google/devtools/build/lib/util:abrupt_exit_exception",
8990
"//src/main/java/com/google/devtools/build/lib/util:exit_code",

0 commit comments

Comments
 (0)