chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 1 | // Copyright 2019 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.remote; |
| 15 | |
| 16 | import static com.google.common.base.Throwables.throwIfInstanceOf; |
| 17 | import static com.google.common.truth.Truth.assertThat; |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 18 | import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.createTreeArtifactWithGeneratingAction; |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 19 | import static java.nio.charset.StandardCharsets.UTF_8; |
| 20 | import static org.junit.Assert.assertThrows; |
| 21 | import static org.mockito.ArgumentMatchers.any; |
Googler | ebd6e58 | 2022-10-24 05:30:19 -0700 | [diff] [blame] | 22 | import static org.mockito.ArgumentMatchers.eq; |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 23 | import static org.mockito.Mockito.doAnswer; |
Googler | ebd6e58 | 2022-10-24 05:30:19 -0700 | [diff] [blame] | 24 | import static org.mockito.Mockito.never; |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 25 | import static org.mockito.Mockito.spy; |
Googler | ebd6e58 | 2022-10-24 05:30:19 -0700 | [diff] [blame] | 26 | import static org.mockito.Mockito.verify; |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 27 | |
| 28 | import com.google.common.collect.ImmutableList; |
| 29 | import com.google.common.collect.ImmutableMap; |
| 30 | import com.google.common.hash.HashCode; |
| 31 | import com.google.common.util.concurrent.Futures; |
| 32 | import com.google.common.util.concurrent.ListenableFuture; |
| 33 | import com.google.common.util.concurrent.SettableFuture; |
| 34 | import com.google.devtools.build.lib.actions.ActionInput; |
| 35 | import com.google.devtools.build.lib.actions.Artifact; |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 36 | import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact; |
| 37 | import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact; |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 38 | import com.google.devtools.build.lib.actions.ArtifactRoot; |
| 39 | import com.google.devtools.build.lib.actions.ArtifactRoot.RootType; |
| 40 | import com.google.devtools.build.lib.actions.FileArtifactValue; |
| 41 | import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue; |
| 42 | import com.google.devtools.build.lib.actions.MetadataProvider; |
| 43 | import com.google.devtools.build.lib.actions.util.ActionsTestUtil; |
| 44 | import com.google.devtools.build.lib.clock.JavaClock; |
| 45 | import com.google.devtools.build.lib.remote.common.BulkTransferException; |
| 46 | import com.google.devtools.build.lib.remote.util.StaticMetadataProvider; |
| 47 | import com.google.devtools.build.lib.remote.util.TempPathGenerator; |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 48 | import com.google.devtools.build.lib.skyframe.TreeArtifactValue; |
| 49 | import com.google.devtools.build.lib.util.Pair; |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 50 | import com.google.devtools.build.lib.vfs.DigestHashFunction; |
| 51 | import com.google.devtools.build.lib.vfs.FileSystem; |
| 52 | import com.google.devtools.build.lib.vfs.FileSystemUtils; |
| 53 | import com.google.devtools.build.lib.vfs.Path; |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 54 | import com.google.devtools.build.lib.vfs.PathFragment; |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 55 | import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; |
| 56 | import java.io.IOException; |
| 57 | import java.util.HashMap; |
| 58 | import java.util.Map; |
| 59 | import java.util.concurrent.ExecutionException; |
| 60 | import java.util.concurrent.Semaphore; |
| 61 | import java.util.concurrent.atomic.AtomicBoolean; |
| 62 | import java.util.function.Supplier; |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 63 | import javax.annotation.Nullable; |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 64 | import org.junit.Before; |
| 65 | import org.junit.Test; |
| 66 | |
| 67 | /** Base test class for {@link AbstractActionInputPrefetcher} implementations. */ |
| 68 | public abstract class ActionInputPrefetcherTestBase { |
| 69 | protected static final DigestHashFunction HASH_FUNCTION = DigestHashFunction.SHA256; |
| 70 | |
| 71 | protected FileSystem fs; |
| 72 | protected Path execRoot; |
| 73 | protected ArtifactRoot artifactRoot; |
| 74 | protected TempPathGenerator tempPathGenerator; |
| 75 | |
| 76 | @Before |
| 77 | public void setUp() throws IOException { |
| 78 | fs = new InMemoryFileSystem(new JavaClock(), HASH_FUNCTION); |
| 79 | execRoot = fs.getPath("/exec"); |
| 80 | execRoot.createDirectoryAndParents(); |
| 81 | artifactRoot = ArtifactRoot.asDerivedRoot(execRoot, RootType.Output, "root"); |
| 82 | artifactRoot.getRoot().asPath().createDirectoryAndParents(); |
| 83 | Path tempDir = fs.getPath("/tmp"); |
| 84 | tempDir.createDirectoryAndParents(); |
| 85 | tempPathGenerator = new TempPathGenerator(tempDir); |
| 86 | } |
| 87 | |
| 88 | protected Artifact createRemoteArtifact( |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 89 | String pathFragment, |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 90 | String contents, |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 91 | @Nullable PathFragment materializationExecPath, |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 92 | Map<ActionInput, FileArtifactValue> metadata, |
| 93 | Map<HashCode, byte[]> cas) { |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 94 | Path p = artifactRoot.getRoot().getRelative(pathFragment); |
| 95 | Artifact a = ActionsTestUtil.createArtifact(artifactRoot, p); |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 96 | byte[] contentsBytes = contents.getBytes(UTF_8); |
| 97 | HashCode hashCode = HASH_FUNCTION.getHashFunction().hashBytes(contentsBytes); |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 98 | RemoteFileArtifactValue f = |
Googler | de02e3f | 2022-09-29 02:47:24 -0700 | [diff] [blame] | 99 | RemoteFileArtifactValue.create( |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 100 | hashCode.asBytes(), |
| 101 | contentsBytes.length, |
| 102 | /* locationIndex= */ 1, |
| 103 | "action-id", |
| 104 | materializationExecPath); |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 105 | metadata.put(a, f); |
| 106 | cas.put(hashCode, contentsBytes); |
| 107 | return a; |
| 108 | } |
| 109 | |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 110 | protected Artifact createRemoteArtifact( |
| 111 | String pathFragment, |
| 112 | String contents, |
| 113 | Map<ActionInput, FileArtifactValue> metadata, |
| 114 | Map<HashCode, byte[]> cas) { |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 115 | return createRemoteArtifact( |
| 116 | pathFragment, contents, /* materializationExecPath= */ null, metadata, cas); |
| 117 | } |
| 118 | |
| 119 | protected Pair<SpecialArtifact, ImmutableList<TreeFileArtifact>> createRemoteTreeArtifact( |
| 120 | String pathFragment, |
| 121 | Map<String, String> contentMap, |
| 122 | @Nullable PathFragment materializationExecPath, |
| 123 | Map<ActionInput, FileArtifactValue> metadata, |
| 124 | Map<HashCode, byte[]> cas) |
| 125 | throws IOException { |
| 126 | SpecialArtifact parent = createTreeArtifactWithGeneratingAction(artifactRoot, pathFragment); |
| 127 | parent.getPath().createDirectoryAndParents(); |
| 128 | parent.getPath().chmod(0555); |
| 129 | TreeArtifactValue.Builder treeBuilder = TreeArtifactValue.newBuilder(parent); |
| 130 | for (Map.Entry<String, String> entry : contentMap.entrySet()) { |
| 131 | byte[] contentsBytes = entry.getValue().getBytes(UTF_8); |
| 132 | HashCode hashCode = HASH_FUNCTION.getHashFunction().hashBytes(contentsBytes); |
| 133 | TreeFileArtifact child = |
| 134 | TreeFileArtifact.createTreeOutput(parent, PathFragment.create(entry.getKey())); |
| 135 | RemoteFileArtifactValue childValue = |
| 136 | RemoteFileArtifactValue.create( |
| 137 | hashCode.asBytes(), contentsBytes.length, /* locationIndex= */ 1, "action-id"); |
| 138 | treeBuilder.putChild(child, childValue); |
| 139 | metadata.put(child, childValue); |
| 140 | cas.put(hashCode, contentsBytes); |
| 141 | } |
| 142 | if (materializationExecPath != null) { |
| 143 | treeBuilder.setMaterializationExecPath(materializationExecPath); |
| 144 | } |
| 145 | TreeArtifactValue treeValue = treeBuilder.build(); |
| 146 | metadata.put(parent, treeValue.getMetadata()); |
| 147 | return Pair.of(parent, treeValue.getChildren().asList()); |
| 148 | } |
| 149 | |
| 150 | protected Pair<SpecialArtifact, ImmutableList<TreeFileArtifact>> createRemoteTreeArtifact( |
| 151 | String pathFragment, |
| 152 | Map<String, String> contentMap, |
| 153 | Map<ActionInput, FileArtifactValue> metadata, |
| 154 | Map<HashCode, byte[]> cas) |
| 155 | throws IOException { |
| 156 | return createRemoteTreeArtifact( |
| 157 | pathFragment, contentMap, /* materializationExecPath= */ null, metadata, cas); |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 158 | } |
| 159 | |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 160 | protected abstract AbstractActionInputPrefetcher createPrefetcher(Map<HashCode, byte[]> cas); |
| 161 | |
| 162 | @Test |
Googler | ebd6e58 | 2022-10-24 05:30:19 -0700 | [diff] [blame] | 163 | public void prefetchFiles_fileExists_doNotDownload() throws IOException, InterruptedException { |
| 164 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 165 | Map<HashCode, byte[]> cas = new HashMap<>(); |
| 166 | Artifact a = createRemoteArtifact("file", "hello world", metadata, cas); |
| 167 | FileSystemUtils.writeContent(a.getPath(), "hello world".getBytes(UTF_8)); |
| 168 | MetadataProvider metadataProvider = new StaticMetadataProvider(metadata); |
| 169 | AbstractActionInputPrefetcher prefetcher = spy(createPrefetcher(cas)); |
| 170 | |
| 171 | wait(prefetcher.prefetchFiles(metadata.keySet(), metadataProvider)); |
| 172 | |
| 173 | verify(prefetcher, never()).doDownloadFile(any(), any(), any(), any()); |
| 174 | assertThat(prefetcher.downloadedFiles()).containsExactly(a.getPath()); |
| 175 | assertThat(prefetcher.downloadsInProgress()).isEmpty(); |
| 176 | } |
| 177 | |
| 178 | @Test |
| 179 | public void prefetchFiles_fileExistsButContentMismatches_download() |
| 180 | throws IOException, InterruptedException { |
| 181 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 182 | Map<HashCode, byte[]> cas = new HashMap<>(); |
| 183 | Artifact a = createRemoteArtifact("file", "hello world remote", metadata, cas); |
| 184 | FileSystemUtils.writeContent(a.getPath(), "hello world local".getBytes(UTF_8)); |
| 185 | MetadataProvider metadataProvider = new StaticMetadataProvider(metadata); |
| 186 | AbstractActionInputPrefetcher prefetcher = spy(createPrefetcher(cas)); |
| 187 | |
| 188 | wait(prefetcher.prefetchFiles(metadata.keySet(), metadataProvider)); |
| 189 | |
| 190 | verify(prefetcher).doDownloadFile(any(), eq(a.getExecPath()), any(), any()); |
| 191 | assertThat(prefetcher.downloadedFiles()).containsExactly(a.getPath()); |
| 192 | assertThat(prefetcher.downloadsInProgress()).isEmpty(); |
| 193 | assertThat(FileSystemUtils.readContent(a.getPath(), UTF_8)).isEqualTo("hello world remote"); |
| 194 | } |
| 195 | |
| 196 | @Test |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 197 | public void prefetchFiles_downloadRemoteFiles() throws Exception { |
| 198 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 199 | Map<HashCode, byte[]> cas = new HashMap<>(); |
| 200 | Artifact a1 = createRemoteArtifact("file1", "hello world", metadata, cas); |
| 201 | Artifact a2 = createRemoteArtifact("file2", "fizz buzz", metadata, cas); |
| 202 | MetadataProvider metadataProvider = new StaticMetadataProvider(metadata); |
| 203 | AbstractActionInputPrefetcher prefetcher = createPrefetcher(cas); |
| 204 | |
| 205 | wait(prefetcher.prefetchFiles(metadata.keySet(), metadataProvider)); |
| 206 | |
| 207 | assertThat(FileSystemUtils.readContent(a1.getPath(), UTF_8)).isEqualTo("hello world"); |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 208 | assertReadableNonWritableAndExecutable(a1.getPath()); |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 209 | assertThat(FileSystemUtils.readContent(a2.getPath(), UTF_8)).isEqualTo("fizz buzz"); |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 210 | assertReadableNonWritableAndExecutable(a2.getPath()); |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 211 | assertThat(prefetcher.downloadedFiles()).containsExactly(a1.getPath(), a2.getPath()); |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 212 | assertThat(prefetcher.downloadsInProgress()).isEmpty(); |
| 213 | } |
| 214 | |
| 215 | @Test |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 216 | public void prefetchFiles_downloadRemoteFiles_withmaterializationExecPath() throws Exception { |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 217 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 218 | Map<HashCode, byte[]> cas = new HashMap<>(); |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 219 | PathFragment targetExecPath = artifactRoot.getExecPath().getChild("target"); |
| 220 | Artifact a = createRemoteArtifact("file", "hello world", targetExecPath, metadata, cas); |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 221 | MetadataProvider metadataProvider = new StaticMetadataProvider(metadata); |
| 222 | AbstractActionInputPrefetcher prefetcher = createPrefetcher(cas); |
| 223 | |
| 224 | wait(prefetcher.prefetchFiles(metadata.keySet(), metadataProvider)); |
| 225 | |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 226 | assertThat(a.getPath().isSymbolicLink()).isTrue(); |
| 227 | assertThat(a.getPath().readSymbolicLink()) |
| 228 | .isEqualTo(execRoot.getRelative(targetExecPath).asFragment()); |
| 229 | assertThat(FileSystemUtils.readContent(a.getPath(), UTF_8)).isEqualTo("hello world"); |
| 230 | assertThat(prefetcher.downloadedFiles()) |
| 231 | .containsExactly(a.getPath(), execRoot.getRelative(targetExecPath)); |
| 232 | assertThat(prefetcher.downloadsInProgress()).isEmpty(); |
| 233 | } |
| 234 | |
| 235 | @Test |
| 236 | public void prefetchFiles_downloadRemoteTrees() throws Exception { |
| 237 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 238 | Map<HashCode, byte[]> cas = new HashMap<>(); |
| 239 | Pair<SpecialArtifact, ImmutableList<TreeFileArtifact>> tree = |
| 240 | createRemoteTreeArtifact( |
| 241 | "dir", |
| 242 | ImmutableMap.of("file1", "content1", "nested_dir/file2", "content2"), |
| 243 | metadata, |
| 244 | cas); |
| 245 | SpecialArtifact parent = tree.getFirst(); |
| 246 | Artifact firstChild = tree.getSecond().get(0); |
| 247 | Artifact secondChild = tree.getSecond().get(1); |
| 248 | |
| 249 | MetadataProvider metadataProvider = new StaticMetadataProvider(metadata); |
| 250 | AbstractActionInputPrefetcher prefetcher = createPrefetcher(cas); |
| 251 | |
| 252 | wait(prefetcher.prefetchFiles(tree.getSecond(), metadataProvider)); |
| 253 | |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 254 | assertReadableNonWritableAndExecutable(parent.getPath()); |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 255 | assertThat(FileSystemUtils.readContent(firstChild.getPath(), UTF_8)).isEqualTo("content1"); |
| 256 | assertReadableNonWritableAndExecutable(firstChild.getPath()); |
| 257 | assertThat(FileSystemUtils.readContent(secondChild.getPath(), UTF_8)).isEqualTo("content2"); |
| 258 | assertReadableNonWritableAndExecutable(secondChild.getPath()); |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 259 | assertThat(prefetcher.downloadedFiles()).containsExactly(parent.getPath()); |
| 260 | assertThat(prefetcher.downloadsInProgress()).isEmpty(); |
| 261 | assertReadableNonWritableAndExecutable(parent.getPath().getRelative("nested_dir")); |
| 262 | } |
| 263 | |
| 264 | @Test |
Tiago Quelhas | 32b0f5a | 2022-10-20 07:05:34 -0700 | [diff] [blame] | 265 | public void prefetchFiles_downloadRemoteTrees_withmaterializationExecPath() throws Exception { |
| 266 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 267 | Map<HashCode, byte[]> cas = new HashMap<>(); |
| 268 | PathFragment targetExecPath = artifactRoot.getExecPath().getChild("target"); |
| 269 | Pair<SpecialArtifact, ImmutableList<TreeFileArtifact>> tree = |
| 270 | createRemoteTreeArtifact( |
| 271 | "dir", |
| 272 | ImmutableMap.of("file1", "content1", "nested_dir/file2", "content2"), |
| 273 | targetExecPath, |
| 274 | metadata, |
| 275 | cas); |
| 276 | SpecialArtifact parent = tree.getFirst(); |
| 277 | Artifact firstChild = tree.getSecond().get(0); |
| 278 | Artifact secondChild = tree.getSecond().get(1); |
| 279 | |
| 280 | MetadataProvider metadataProvider = new StaticMetadataProvider(metadata); |
| 281 | AbstractActionInputPrefetcher prefetcher = createPrefetcher(cas); |
| 282 | |
| 283 | wait(prefetcher.prefetchFiles(tree.getSecond(), metadataProvider)); |
| 284 | |
| 285 | assertThat(parent.getPath().isSymbolicLink()).isTrue(); |
| 286 | assertThat(parent.getPath().readSymbolicLink()) |
| 287 | .isEqualTo(execRoot.getRelative(targetExecPath).asFragment()); |
| 288 | assertReadableNonWritableAndExecutable(parent.getPath()); |
| 289 | assertThat(FileSystemUtils.readContent(firstChild.getPath(), UTF_8)).isEqualTo("content1"); |
| 290 | assertReadableNonWritableAndExecutable(firstChild.getPath()); |
| 291 | assertThat(FileSystemUtils.readContent(secondChild.getPath(), UTF_8)).isEqualTo("content2"); |
| 292 | assertReadableNonWritableAndExecutable(secondChild.getPath()); |
| 293 | assertThat(prefetcher.downloadedFiles()) |
| 294 | .containsExactly(parent.getPath(), execRoot.getRelative(targetExecPath)); |
| 295 | assertThat(prefetcher.downloadsInProgress()).isEmpty(); |
| 296 | assertReadableNonWritableAndExecutable(parent.getPath().getRelative("nested_dir")); |
| 297 | } |
| 298 | |
| 299 | @Test |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 300 | public void prefetchFiles_missingFiles_fails() throws Exception { |
| 301 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 302 | Artifact a = createRemoteArtifact("file1", "hello world", metadata, /* cas= */ new HashMap<>()); |
| 303 | MetadataProvider metadataProvider = new StaticMetadataProvider(metadata); |
| 304 | AbstractActionInputPrefetcher prefetcher = createPrefetcher(new HashMap<>()); |
| 305 | |
| 306 | assertThrows( |
| 307 | BulkTransferException.class, |
| 308 | () -> wait(prefetcher.prefetchFiles(ImmutableList.of(a), metadataProvider))); |
| 309 | |
| 310 | assertThat(prefetcher.downloadedFiles()).isEmpty(); |
| 311 | assertThat(prefetcher.downloadsInProgress()).isEmpty(); |
| 312 | } |
| 313 | |
| 314 | @Test |
| 315 | public void prefetchFiles_ignoreNonRemoteFiles() throws Exception { |
| 316 | // Test that files that are not remote are not downloaded |
| 317 | |
| 318 | Path p = execRoot.getRelative(artifactRoot.getExecPath()).getRelative("file1"); |
| 319 | FileSystemUtils.writeContent(p, UTF_8, "hello world"); |
| 320 | Artifact a = ActionsTestUtil.createArtifact(artifactRoot, p); |
| 321 | FileArtifactValue f = FileArtifactValue.createForTesting(a); |
| 322 | MetadataProvider metadataProvider = new StaticMetadataProvider(ImmutableMap.of(a, f)); |
| 323 | AbstractActionInputPrefetcher prefetcher = createPrefetcher(new HashMap<>()); |
| 324 | |
| 325 | wait(prefetcher.prefetchFiles(ImmutableList.of(a), metadataProvider)); |
| 326 | |
| 327 | assertThat(prefetcher.downloadedFiles()).isEmpty(); |
| 328 | assertThat(prefetcher.downloadsInProgress()).isEmpty(); |
| 329 | } |
| 330 | |
| 331 | @Test |
| 332 | public void prefetchFiles_multipleThreads_downloadIsCancelled() throws Exception { |
| 333 | // Test shared downloads are cancelled if all threads/callers are interrupted |
| 334 | |
| 335 | // arrange |
| 336 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 337 | Map<HashCode, byte[]> cas = new HashMap<>(); |
| 338 | Artifact artifact = createRemoteArtifact("file1", "hello world", metadata, cas); |
| 339 | MetadataProvider metadataProvider = new StaticMetadataProvider(metadata); |
| 340 | |
| 341 | AbstractActionInputPrefetcher prefetcher = spy(createPrefetcher(cas)); |
| 342 | SettableFuture<Void> downloadThatNeverFinishes = SettableFuture.create(); |
| 343 | mockDownload(prefetcher, cas, () -> downloadThatNeverFinishes); |
| 344 | |
| 345 | Thread cancelledThread1 = |
| 346 | new Thread( |
| 347 | () -> { |
| 348 | try { |
| 349 | wait(prefetcher.prefetchFiles(ImmutableList.of(artifact), metadataProvider)); |
| 350 | } catch (IOException | InterruptedException ignored) { |
| 351 | // do nothing |
| 352 | } |
| 353 | }); |
| 354 | |
| 355 | Thread cancelledThread2 = |
| 356 | new Thread( |
| 357 | () -> { |
| 358 | try { |
| 359 | wait(prefetcher.prefetchFiles(ImmutableList.of(artifact), metadataProvider)); |
| 360 | } catch (IOException | InterruptedException ignored) { |
| 361 | // do nothing |
| 362 | } |
| 363 | }); |
| 364 | |
| 365 | // act |
| 366 | cancelledThread1.start(); |
| 367 | cancelledThread2.start(); |
| 368 | cancelledThread1.interrupt(); |
| 369 | cancelledThread2.interrupt(); |
| 370 | cancelledThread1.join(); |
| 371 | cancelledThread2.join(); |
| 372 | |
| 373 | // assert |
| 374 | assertThat(downloadThatNeverFinishes.isCancelled()).isTrue(); |
| 375 | assertThat(artifact.getPath().exists()).isFalse(); |
| 376 | assertThat(tempPathGenerator.getTempDir().getDirectoryEntries()).isEmpty(); |
| 377 | } |
| 378 | |
| 379 | @Test |
| 380 | public void prefetchFiles_multipleThreads_downloadIsNotCancelledByOtherThreads() |
| 381 | throws Exception { |
| 382 | // Test multiple threads can share downloads, but do not cancel each other when interrupted |
| 383 | |
| 384 | // arrange |
| 385 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 386 | Map<HashCode, byte[]> cas = new HashMap<>(); |
| 387 | Artifact artifact = createRemoteArtifact("file1", "hello world", metadata, cas); |
| 388 | MetadataProvider metadataProvider = new StaticMetadataProvider(metadata); |
| 389 | SettableFuture<Void> download = SettableFuture.create(); |
| 390 | AbstractActionInputPrefetcher prefetcher = spy(createPrefetcher(cas)); |
| 391 | mockDownload(prefetcher, cas, () -> download); |
| 392 | Thread cancelledThread = |
| 393 | new Thread( |
| 394 | () -> { |
| 395 | try { |
| 396 | wait(prefetcher.prefetchFiles(ImmutableList.of(artifact), metadataProvider)); |
| 397 | } catch (IOException | InterruptedException ignored) { |
| 398 | // do nothing |
| 399 | } |
| 400 | }); |
| 401 | |
| 402 | AtomicBoolean successful = new AtomicBoolean(false); |
| 403 | Thread successfulThread = |
| 404 | new Thread( |
| 405 | () -> { |
| 406 | try { |
| 407 | wait(prefetcher.prefetchFiles(ImmutableList.of(artifact), metadataProvider)); |
| 408 | successful.set(true); |
| 409 | } catch (IOException | InterruptedException ignored) { |
| 410 | // do nothing |
| 411 | } |
| 412 | }); |
| 413 | cancelledThread.start(); |
| 414 | successfulThread.start(); |
| 415 | while (true) { |
| 416 | if (prefetcher |
| 417 | .getDownloadCache() |
| 418 | .getSubscriberCount(execRoot.getRelative(artifact.getExecPath())) |
| 419 | == 2) { |
| 420 | break; |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | // act |
| 425 | cancelledThread.interrupt(); |
| 426 | cancelledThread.join(); |
| 427 | // simulate the download finishing |
| 428 | assertThat(download.isCancelled()).isFalse(); |
| 429 | download.set(null); |
| 430 | successfulThread.join(); |
| 431 | |
| 432 | // assert |
| 433 | assertThat(successful.get()).isTrue(); |
| 434 | assertThat(FileSystemUtils.readContent(artifact.getPath(), UTF_8)).isEqualTo("hello world"); |
| 435 | } |
| 436 | |
| 437 | @Test |
| 438 | public void downloadFile_downloadRemoteFiles() throws Exception { |
| 439 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 440 | Map<HashCode, byte[]> cas = new HashMap<>(); |
| 441 | Artifact a1 = createRemoteArtifact("file1", "hello world", metadata, cas); |
| 442 | AbstractActionInputPrefetcher prefetcher = createPrefetcher(cas); |
| 443 | |
| 444 | prefetcher.downloadFile(a1.getPath(), metadata.get(a1)); |
| 445 | |
| 446 | assertThat(FileSystemUtils.readContent(a1.getPath(), UTF_8)).isEqualTo("hello world"); |
| 447 | assertThat(a1.getPath().isExecutable()).isTrue(); |
| 448 | assertThat(a1.getPath().isReadable()).isTrue(); |
| 449 | assertThat(a1.getPath().isWritable()).isFalse(); |
| 450 | } |
| 451 | |
| 452 | @Test |
| 453 | public void downloadFile_onInterrupt_deletePartialDownloadedFile() throws Exception { |
| 454 | Semaphore startSemaphore = new Semaphore(0); |
| 455 | Semaphore endSemaphore = new Semaphore(0); |
| 456 | Map<ActionInput, FileArtifactValue> metadata = new HashMap<>(); |
| 457 | Map<HashCode, byte[]> cas = new HashMap<>(); |
| 458 | Artifact a1 = createRemoteArtifact("file1", "hello world", metadata, cas); |
| 459 | AbstractActionInputPrefetcher prefetcher = spy(createPrefetcher(cas)); |
| 460 | mockDownload( |
| 461 | prefetcher, |
| 462 | cas, |
| 463 | () -> { |
| 464 | startSemaphore.release(); |
| 465 | return SettableFuture.create(); // A future that never complete so we can interrupt later |
| 466 | }); |
| 467 | |
| 468 | AtomicBoolean interrupted = new AtomicBoolean(false); |
| 469 | Thread t = |
| 470 | new Thread( |
| 471 | () -> { |
| 472 | try { |
| 473 | prefetcher.downloadFile(a1.getPath(), metadata.get(a1)); |
| 474 | } catch (IOException ignored) { |
| 475 | // Intentionally left empty |
| 476 | } catch (InterruptedException e) { |
| 477 | interrupted.set(true); |
| 478 | } |
| 479 | endSemaphore.release(); |
| 480 | }); |
| 481 | t.start(); |
| 482 | startSemaphore.acquire(); |
| 483 | t.interrupt(); |
| 484 | endSemaphore.acquire(); |
| 485 | |
| 486 | assertThat(interrupted.get()).isTrue(); |
| 487 | assertThat(a1.getPath().exists()).isFalse(); |
| 488 | assertThat(tempPathGenerator.getTempDir().getDirectoryEntries()).isEmpty(); |
| 489 | } |
| 490 | |
| 491 | protected static void wait(ListenableFuture<Void> future) |
| 492 | throws IOException, InterruptedException { |
| 493 | try { |
| 494 | future.get(); |
| 495 | } catch (ExecutionException e) { |
| 496 | Throwable cause = e.getCause(); |
| 497 | if (cause != null) { |
| 498 | throwIfInstanceOf(cause, IOException.class); |
| 499 | throwIfInstanceOf(cause, InterruptedException.class); |
| 500 | throwIfInstanceOf(cause, RuntimeException.class); |
| 501 | } |
| 502 | throw new IOException(e); |
| 503 | } catch (InterruptedException e) { |
| 504 | future.cancel(/*mayInterruptIfRunning=*/ true); |
| 505 | throw e; |
| 506 | } |
| 507 | } |
| 508 | |
| 509 | protected static void mockDownload( |
| 510 | AbstractActionInputPrefetcher prefetcher, |
| 511 | Map<HashCode, byte[]> cas, |
| 512 | Supplier<ListenableFuture<Void>> resultSupplier) |
| 513 | throws IOException { |
| 514 | doAnswer( |
| 515 | invocation -> { |
| 516 | Path path = invocation.getArgument(0); |
Googler | 3e83fbe | 2022-10-18 04:12:56 -0700 | [diff] [blame] | 517 | FileArtifactValue metadata = invocation.getArgument(2); |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 518 | byte[] content = cas.get(HashCode.fromBytes(metadata.getDigest())); |
| 519 | if (content == null) { |
| 520 | return Futures.immediateFailedFuture(new IOException("Not found")); |
| 521 | } |
| 522 | FileSystemUtils.writeContent(path, content); |
| 523 | return resultSupplier.get(); |
| 524 | }) |
| 525 | .when(prefetcher) |
Googler | 3e83fbe | 2022-10-18 04:12:56 -0700 | [diff] [blame] | 526 | .doDownloadFile(any(), any(), any(), any()); |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 527 | } |
Googler | ffc560d | 2022-06-23 04:30:57 -0700 | [diff] [blame] | 528 | |
| 529 | private void assertReadableNonWritableAndExecutable(Path path) throws IOException { |
| 530 | assertThat(path.isReadable()).isTrue(); |
| 531 | assertThat(path.isWritable()).isFalse(); |
| 532 | assertThat(path.isExecutable()).isTrue(); |
| 533 | } |
chiwang | 8cea765 | 2022-05-23 02:24:47 -0700 | [diff] [blame] | 534 | } |