Skip to content

Commit f201504

Browse files
committed
Fix disk cache failures on concurrent read-write access on Windows
This applies the fix made to the download cache in 753dc97 to the disk cache.
1 parent bb5a23d commit f201504

File tree

6 files changed

+123
-3
lines changed

6 files changed

+123
-3
lines changed

src/main/java/com/google/devtools/build/lib/bazel/repository/cache/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ java_library(
2323
deps = [
2424
"//src/main/java/com/google/devtools/build/lib/server:idle_task",
2525
"//src/main/java/com/google/devtools/build/lib/util:file_system_lock",
26+
"//src/main/java/com/google/devtools/build/lib/util:os",
2627
"//src/main/java/com/google/devtools/build/lib/vfs",
2728
"//third_party:guava",
2829
"//third_party:jsr305",

src/main/java/com/google/devtools/build/lib/bazel/repository/cache/DownloadCache.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.common.hash.HashFunction;
2222
import com.google.common.hash.Hasher;
2323
import com.google.common.io.BaseEncoding;
24+
import com.google.devtools.build.lib.util.OS;
2425
import com.google.devtools.build.lib.vfs.DigestHashFunction;
2526
import com.google.devtools.build.lib.vfs.FileAccessException;
2627
import com.google.devtools.build.lib.vfs.FileSystemUtils;
@@ -251,7 +252,7 @@ private void storeCacheValue(
251252
// that another thread won the race to place the file in the cache. As the exception is rather
252253
// generic and could result from other failure types, we rethrow the exception if the cache
253254
// entry hasn't been created.
254-
if (!cacheValue.exists()) {
255+
if (OS.getCurrent() != OS.WINDOWS || !cacheValue.exists()) {
255256
throw e;
256257
}
257258
}

src/main/java/com/google/devtools/build/lib/remote/disk/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ java_library(
2424
"//src/main/java/com/google/devtools/build/lib/remote/util:digest_utils",
2525
"//src/main/java/com/google/devtools/build/lib/server:idle_task",
2626
"//src/main/java/com/google/devtools/build/lib/util:file_system_lock",
27+
"//src/main/java/com/google/devtools/build/lib/util:os",
2728
"//src/main/java/com/google/devtools/build/lib/vfs",
2829
"//third_party:flogger",
2930
"//third_party:guava",

src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheClient.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import com.google.devtools.build.lib.remote.util.DigestOutputStream;
3636
import com.google.devtools.build.lib.remote.util.DigestUtil;
3737
import com.google.devtools.build.lib.remote.util.Utils;
38+
import com.google.devtools.build.lib.util.OS;
39+
import com.google.devtools.build.lib.vfs.FileAccessException;
3840
import com.google.devtools.build.lib.vfs.Path;
3941
import com.google.protobuf.ByteString;
4042
import com.google.protobuf.ExtensionRegistryLite;
@@ -142,7 +144,20 @@ public void captureFile(Path src, Digest digest, Store store) throws IOException
142144
}
143145

144146
target.getParentDirectory().createDirectoryAndParents();
145-
src.renameTo(target);
147+
try {
148+
src.renameTo(target);
149+
} catch (FileAccessException e) {
150+
// On Windows, atomically replacing a file that is currently opened (e.g. due to a
151+
// concurrent get on the cache) results in renameTo throwing this exception, which wraps an
152+
// AccessDeniedException. This case is benign since if the target path already exists, we
153+
// know that another thread won the race to place the file in the cache. As the exception is
154+
// rather generic and could result from other failure types, we rethrow the exception if the
155+
// cache entry hasn't been created.
156+
if (OS.getCurrent() != OS.WINDOWS || !target.exists()) {
157+
throw e;
158+
}
159+
src.delete();
160+
}
146161
}
147162

148163
private ListenableFuture<Void> download(Digest digest, OutputStream out, Store store) {
@@ -333,7 +348,20 @@ public void saveFile(Digest digest, Store store, InputStream in) throws IOExcept
333348
}
334349
}
335350
path.getParentDirectory().createDirectoryAndParents();
336-
temp.renameTo(path);
351+
try {
352+
temp.renameTo(path);
353+
} catch (FileAccessException e) {
354+
// On Windows, atomically replacing a file that is currently opened (e.g. due to a
355+
// concurrent get on the cache) results in renameTo throwing this exception, which wraps an
356+
// AccessDeniedException. This case is benign since if the target path already exists, we
357+
// know that another thread won the race to place the file in the cache. As the exception is
358+
// rather generic and could result from other failure types, we rethrow the exception if the
359+
// cache entry hasn't been created.
360+
if (OS.getCurrent() != OS.WINDOWS || !path.exists()) {
361+
throw e;
362+
}
363+
temp.delete();
364+
}
337365
} catch (IOException e) {
338366
try {
339367
temp.delete();

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ java_test(
3333
"//src/test/java/com/google/devtools/build/lib:test_runner",
3434
"//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
3535
"//src/test/java/com/google/devtools/build/lib/testutil:external_file_system_lock",
36+
"//src/test/java/com/google/devtools/build/lib/vfs/util:util_internal",
3637
"//third_party:error_prone_annotations",
3738
"//third_party:guava",
3839
"//third_party:junit4",

src/test/java/com/google/devtools/build/lib/remote/disk/DiskCacheClientTest.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,25 @@
3232
import com.google.devtools.build.lib.remote.common.OutputDigestMismatchException;
3333
import com.google.devtools.build.lib.remote.common.RemoteCacheClient.ActionKey;
3434
import com.google.devtools.build.lib.remote.util.DigestUtil;
35+
import com.google.devtools.build.lib.testutil.TestUtils;
3536
import com.google.devtools.build.lib.vfs.DigestHashFunction;
3637
import com.google.devtools.build.lib.vfs.FileSystem;
3738
import com.google.devtools.build.lib.vfs.FileSystemUtils;
3839
import com.google.devtools.build.lib.vfs.Path;
3940
import com.google.devtools.build.lib.vfs.SyscallCache;
4041
import com.google.devtools.build.lib.vfs.bazel.BazelHashFunctions;
4142
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
43+
import com.google.devtools.build.lib.vfs.util.FileSystems;
4244
import com.google.errorprone.annotations.CanIgnoreReturnValue;
4345
import com.google.protobuf.ByteString;
4446
import com.google.protobuf.Message;
4547
import java.io.IOException;
48+
import java.io.OutputStream;
49+
import java.util.ArrayList;
50+
import java.util.concurrent.CountDownLatch;
51+
import java.util.concurrent.ExecutionException;
52+
import java.util.concurrent.Executors;
53+
import java.util.concurrent.Future;
4654
import org.junit.After;
4755
import org.junit.Before;
4856
import org.junit.Test;
@@ -346,6 +354,86 @@ public void downloadActionResult_withReferencedTreeFileMissing_returnsNull() thr
346354
assertThat(result).isNull();
347355
}
348356

357+
@Test
358+
public void concurrentUploadDownload()
359+
throws IOException, ExecutionException, InterruptedException {
360+
var nativeDiskCacheDir = TestUtils.createUniqueTmpDir(FileSystems.getNativeFileSystem());
361+
var nativeClient =
362+
new DiskCacheClient(nativeDiskCacheDir, DIGEST_UTIL, /* verifyDownloads= */ false);
363+
var tasks = new ArrayList<Future<?>>();
364+
// Use 1 MB blobs to increase the window for concurrent access during write/rename.
365+
var contentSize = 1024 * 1024;
366+
var numConcurrentOps = 10;
367+
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
368+
for (int attempt = 0; attempt < 100; attempt++) {
369+
var contentArray = new byte[contentSize];
370+
// Fill with a pattern based on the attempt number.
371+
for (int i = 0; i < contentSize; i++) {
372+
contentArray[i] = (byte) (attempt + i);
373+
}
374+
var contentBytes = ByteString.copyFrom(contentArray);
375+
var contentDigest = DIGEST_UTIL.compute(contentArray);
376+
// Use a latch to ensure all concurrent tasks start at roughly the same time.
377+
var startLatch = new CountDownLatch(numConcurrentOps);
378+
// Half the tasks do uploads, half do downloads with a slow OutputStream to keep the file
379+
// open longer. This maximizes the chance of a rename failing because a download has the
380+
// file open.
381+
for (int concurrentOp = 0; concurrentOp < numConcurrentOps; concurrentOp++) {
382+
boolean isUploader = concurrentOp % 2 == 0;
383+
tasks.add(
384+
executor.submit(
385+
() -> {
386+
// Signal ready and wait for all tasks to be ready.
387+
startLatch.countDown();
388+
startLatch.await();
389+
if (isUploader) {
390+
getFromFuture(nativeClient.uploadBlob(contentDigest, contentBytes));
391+
} else {
392+
// Use a slow OutputStream that pauses periodically to keep the file open
393+
// longer during download.
394+
var out =
395+
new OutputStream() {
396+
private int bytesWritten = 0;
397+
398+
@Override
399+
public void write(int b) throws IOException {
400+
bytesWritten++;
401+
maybeSleep();
402+
}
403+
404+
@Override
405+
public void write(byte[] b, int off, int len) throws IOException {
406+
bytesWritten += len;
407+
maybeSleep();
408+
}
409+
410+
private void maybeSleep() {
411+
// Sleep every 64KB to slow down the download.
412+
if (bytesWritten % (64 * 1024) < 100) {
413+
try {
414+
Thread.sleep(1);
415+
} catch (InterruptedException e) {
416+
Thread.currentThread().interrupt();
417+
}
418+
}
419+
}
420+
};
421+
try {
422+
getFromFuture(nativeClient.downloadBlob(contentDigest, out));
423+
} catch (CacheNotFoundException ignored) {
424+
// File not yet uploaded by another task.
425+
}
426+
}
427+
return null;
428+
}));
429+
}
430+
}
431+
for (var task : tasks) {
432+
task.get();
433+
}
434+
}
435+
}
436+
349437
private Tree getTreeWithFile(Digest fileDigest) {
350438
return Tree.newBuilder()
351439
.addChildren(Directory.newBuilder().addFiles(FileNode.newBuilder().setDigest(fileDigest)))

0 commit comments

Comments
 (0)