Skip to content

Commit 4050120

Browse files
EdSchoutencopybara-github
authored andcommitted
Prevent failures creating output directories
I would receive reports of build failures along these lines: > ERROR: some/path/BUILD:156:32: Executing genrule //some/path:some_target failed: failed to create output directory '.../some/path/foo' In order to root cause this, I made a minor change to Bazel to extend its logging. See bazelbuild#17951. With that change applied, the error became: > ERROR: some/path/BUILD:156:32: Executing genrule //some/path:some_target failed: failed to create output directory '.../some/path/foo': .../some/path/foo (Not a directory) I was eventually able to reproduce this by manually creating an output file at bazel-bin/some/path, followed by attempting to build the target. It looks like Bazel simply attempts to create output directories without taking into consideration whether a file already exists at the target location. This is problematic when someone alters an existing build target to emit a directory instead of a regular file. Note that this only an issue when the remote action file system is used. Plain builds (ones that don't use Builds without the Bytes or bb_clientd) are unaffected. This change addresses this issue by reusing the same fallback path creation strategy that plain builds already use. Closes bazelbuild#17968. PiperOrigin-RevId: 523408378 Change-Id: I0d7559352687c317de72e0bf5bc6b5ba7452f97e
1 parent 088d8e9 commit 4050120

File tree

2 files changed

+99
-61
lines changed

2 files changed

+99
-61
lines changed

src/main/java/com/google/devtools/build/lib/skyframe/ActionOutputDirectoryHelper.java

+67-61
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,7 @@ private enum DirectoryState {
6161

6262
/**
6363
* Creates output directories for an in-memory action file system ({@link
64-
* com.google.devtools.build.lib.vfs.OutputService.ActionFileSystemType#inMemoryFileSystem}). The
65-
* action-local filesystem starts empty, so we expect the output directory creation to always
66-
* succeed. There can be no interference from state left behind by prior builds or other actions
67-
* intra-build.
64+
* com.google.devtools.build.lib.vfs.OutputService.ActionFileSystemType#inMemoryFileSystem}).
6865
*/
6966
void createActionFsOutputDirectories(
7067
ImmutableSet<Artifact> actionOutputs, ArtifactPathResolver artifactPathResolver)
@@ -81,9 +78,13 @@ void createActionFsOutputDirectories(
8178
if (done.add(outputDir)) {
8279
try {
8380
outputDir.createDirectoryAndParents();
81+
continue;
8482
} catch (IOException e) {
85-
throw new CreateOutputDirectoryException(outputDir.asFragment(), e);
83+
/* Fall through to plan B. */
8684
}
85+
86+
Path rootPath = artifactPathResolver.convertPath(outputFile.getRoot().getRoot().asPath());
87+
forceCreateDirectoryAndParents(outputDir, rootPath);
8788
}
8889
}
8990
}
@@ -133,71 +134,77 @@ void createOutputDirectories(ImmutableSet<Artifact> actionOutputs)
133134
}
134135

135136
if (done.add(outputDir)) {
137+
Path rootPath = outputFile.getRoot().getRoot().asPath();
136138
try {
137-
createAndCheckForSymlinks(outputDir, outputFile);
139+
createAndCheckForSymlinks(outputDir, rootPath);
138140
continue;
139141
} catch (IOException e) {
140142
/* Fall through to plan B. */
141143
}
142144

143-
// Possibly some direct ancestors are not directories. In that case, we traverse the
144-
// ancestors downward, deleting any non-directories. This handles the case where a file
145-
// becomes a directory. The traversal is done downward because otherwise we may delete
146-
// files through a symlink in a parent directory. Since Blaze never creates such
147-
// directories within a build, we have no idea where on disk we're actually deleting.
145+
forceCreateDirectoryAndParents(outputDir, rootPath);
146+
}
147+
}
148+
}
149+
150+
void forceCreateDirectoryAndParents(Path outputDir, Path rootPath)
151+
throws CreateOutputDirectoryException {
152+
// Possibly some direct ancestors are not directories. In that case, we traverse the
153+
// ancestors downward, deleting any non-directories. This handles the case where a file
154+
// becomes a directory. The traversal is done downward because otherwise we may delete
155+
// files through a symlink in a parent directory. Since Blaze never creates such
156+
// directories within a build, we have no idea where on disk we're actually deleting.
157+
//
158+
// Symlinks should not be followed so in order to clean up symlinks pointing to Fileset
159+
// outputs from previous builds. See bug [incremental build of Fileset fails if
160+
// Fileset.out was changed to be a subdirectory of the old value].
161+
try {
162+
Path p = rootPath;
163+
for (String segment : outputDir.relativeTo(p).segments()) {
164+
p = p.getRelative(segment);
165+
166+
// This lock ensures that the only thread that observes a filesystem transition in
167+
// which the path p first exists and then does not is the thread that calls
168+
// p.delete() and causes the transition.
169+
//
170+
// If it were otherwise, then some thread A could test p.exists(), see that it does,
171+
// then test p.isDirectory(), see that p isn't a directory (because, say, thread
172+
// B deleted it), and then call p.delete(). That could result in two different kinds
173+
// of failures:
174+
//
175+
// 1) In the time between when thread A sees that p is not a directory and when thread
176+
// A calls p.delete(), thread B may reach the call to createDirectoryAndParents
177+
// and create a directory at p, which thread A then deletes. Thread B would then try
178+
// adding outputs to the directory it thought was there, and fail.
148179
//
149-
// Symlinks should not be followed so in order to clean up symlinks pointing to Fileset
150-
// outputs from previous builds. See bug [incremental build of Fileset fails if
151-
// Fileset.out was changed to be a subdirectory of the old value].
180+
// 2) In the time between when thread A sees that p is not a directory and when thread
181+
// A calls p.delete(), thread B may create a directory at p, and then either create a
182+
// subdirectory beneath it or add outputs to it. Then when thread A tries to delete p,
183+
// it would fail.
184+
Lock lock = outputDirectoryDeletionLock.get(p);
185+
lock.lock();
152186
try {
153-
Path p = outputFile.getRoot().getRoot().asPath();
154-
for (String segment : outputDir.relativeTo(p).segments()) {
155-
p = p.getRelative(segment);
156-
157-
// This lock ensures that the only thread that observes a filesystem transition in
158-
// which the path p first exists and then does not is the thread that calls
159-
// p.delete() and causes the transition.
160-
//
161-
// If it were otherwise, then some thread A could test p.exists(), see that it does,
162-
// then test p.isDirectory(), see that p isn't a directory (because, say, thread
163-
// B deleted it), and then call p.delete(). That could result in two different kinds
164-
// of failures:
165-
//
166-
// 1) In the time between when thread A sees that p is not a directory and when thread
167-
// A calls p.delete(), thread B may reach the call to createDirectoryAndParents
168-
// and create a directory at p, which thread A then deletes. Thread B would then try
169-
// adding outputs to the directory it thought was there, and fail.
170-
//
171-
// 2) In the time between when thread A sees that p is not a directory and when thread
172-
// A calls p.delete(), thread B may create a directory at p, and then either create a
173-
// subdirectory beneath it or add outputs to it. Then when thread A tries to delete p,
174-
// it would fail.
175-
Lock lock = outputDirectoryDeletionLock.get(p);
176-
lock.lock();
177-
try {
178-
FileStatus stat = p.statIfFound(Symlinks.NOFOLLOW);
179-
if (stat == null) {
180-
// Missing entry: Break out and create expected directories.
181-
break;
182-
}
183-
if (stat.isDirectory()) {
184-
// If this directory used to be a tree artifact it won't be writable.
185-
p.setWritable(true);
186-
knownDirectories.put(p.asFragment(), DirectoryState.FOUND);
187-
} else {
188-
// p may be a file or symlink (possibly from a Fileset in a previous build).
189-
p.delete(); // throws IOException
190-
break;
191-
}
192-
} finally {
193-
lock.unlock();
194-
}
187+
FileStatus stat = p.statIfFound(Symlinks.NOFOLLOW);
188+
if (stat == null) {
189+
// Missing entry: Break out and create expected directories.
190+
break;
195191
}
196-
outputDir.createDirectoryAndParents();
197-
} catch (IOException e) {
198-
throw new CreateOutputDirectoryException(outputDir.asFragment(), e);
192+
if (stat.isDirectory()) {
193+
// If this directory used to be a tree artifact it won't be writable.
194+
p.setWritable(true);
195+
knownDirectories.put(p.asFragment(), DirectoryState.FOUND);
196+
} else {
197+
// p may be a file or symlink (possibly from a Fileset in a previous build).
198+
p.delete(); // throws IOException
199+
break;
200+
}
201+
} finally {
202+
lock.unlock();
199203
}
200204
}
205+
outputDir.createDirectoryAndParents();
206+
} catch (IOException e) {
207+
throw new CreateOutputDirectoryException(outputDir.asFragment(), e);
201208
}
202209
}
203210

@@ -206,8 +213,7 @@ void createOutputDirectories(ImmutableSet<Artifact> actionOutputs)
206213
* output file. These are all expected to be regular directories. Violations of this expectations
207214
* can only come from state left behind by previous invocations or external filesystem mutation.
208215
*/
209-
private void createAndCheckForSymlinks(Path dir, Artifact outputFile) throws IOException {
210-
Path rootPath = outputFile.getRoot().getRoot().asPath();
216+
private void createAndCheckForSymlinks(Path dir, Path rootPath) throws IOException {
211217
PathFragment root = rootPath.asFragment();
212218

213219
// If the output root has not been created yet, do so now.

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

+32
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.google.devtools.build.lib.standalone.StandaloneModule;
3232
import com.google.devtools.build.lib.util.OS;
3333
import com.google.devtools.build.lib.vfs.FileSystemUtils;
34+
import com.google.devtools.build.lib.vfs.Path;
3435
import java.io.IOException;
3536
import org.junit.After;
3637
import org.junit.Test;
@@ -432,6 +433,37 @@ public void symlinkToNestedDirectory() throws Exception {
432433
buildTarget("//a:one_local", "//a:two_local", "//a:one_remote", "//a:two_remote");
433434
}
434435

436+
@Test
437+
public void replaceOutputDirectoryWithFile() throws Exception {
438+
write(
439+
"a/defs.bzl",
440+
"def _impl(ctx):",
441+
" dir = ctx.actions.declare_directory(ctx.label.name + '.dir')",
442+
" ctx.actions.run_shell(",
443+
" outputs = [dir],",
444+
" command = 'touch $1/hello',",
445+
" arguments = [dir.path],",
446+
" )",
447+
" return DefaultInfo(files = depset([dir]))",
448+
"",
449+
"my_rule = rule(",
450+
" implementation = _impl,",
451+
")");
452+
write("a/BUILD", "load(':defs.bzl', 'my_rule')", "", "my_rule(name = 'hello')");
453+
454+
setDownloadToplevel();
455+
buildTarget("//a:hello");
456+
457+
// Replace the existing output directory of the package with a file.
458+
// A subsequent build should remove this file and replace it with a
459+
// directory.
460+
Path outputPath = getOutputPath("a");
461+
outputPath.deleteTree();
462+
FileSystemUtils.writeContent(outputPath, new byte[] {1, 2, 3, 4, 5});
463+
464+
buildTarget("//a:hello");
465+
}
466+
435467
@Test
436468
public void remoteCacheEvictBlobs_whenPrefetchingInput_exitWithCode39() throws Exception {
437469
// Arrange: Prepare workspace and populate remote cache

0 commit comments

Comments
 (0)