blob: d978ad6250dea91e80a5260101cd858bcc20e238 [file] [log] [blame]
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001// Copyright 2017 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.
14package com.google.devtools.build.lib.remote;
15
jhorvitzed7ec3b2020-07-24 15:08:03 -070016import static com.google.common.util.concurrent.Futures.immediateFuture;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070017import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
Chi Wang0f812eb2021-07-06 01:14:43 -070018import static com.google.devtools.build.lib.remote.common.ProgressStatusListener.NO_ACTION;
19import static com.google.devtools.build.lib.remote.util.Utils.bytesCountToDisplayString;
George Gensureaeee3e02020-04-15 04:43:45 -070020import static com.google.devtools.build.lib.remote.util.Utils.getFromFuture;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070021
olaolaf0aa55d2018-08-16 08:51:06 -070022import build.bazel.remote.execution.v2.Action;
23import build.bazel.remote.execution.v2.ActionResult;
24import build.bazel.remote.execution.v2.Command;
25import build.bazel.remote.execution.v2.Digest;
26import build.bazel.remote.execution.v2.Directory;
27import build.bazel.remote.execution.v2.DirectoryNode;
28import build.bazel.remote.execution.v2.FileNode;
29import build.bazel.remote.execution.v2.OutputDirectory;
30import build.bazel.remote.execution.v2.OutputFile;
olaolafbfb3cb2018-11-08 11:14:57 -080031import build.bazel.remote.execution.v2.OutputSymlink;
32import build.bazel.remote.execution.v2.SymlinkNode;
olaolaf0aa55d2018-08-16 08:51:06 -070033import build.bazel.remote.execution.v2.Tree;
buchgrff008f42018-06-02 14:13:43 -070034import com.google.common.base.Preconditions;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070035import com.google.common.collect.ImmutableList;
36import com.google.common.collect.ImmutableMap;
37import com.google.common.collect.ImmutableSet;
olaolafbfb3cb2018-11-08 11:14:57 -080038import com.google.common.collect.Iterables;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070039import com.google.common.collect.Maps;
Chi Wangc7c7f5f2020-11-09 22:29:35 -080040import com.google.common.flogger.GoogleLogger;
buchgrff008f42018-06-02 14:13:43 -070041import com.google.common.util.concurrent.FutureCallback;
42import com.google.common.util.concurrent.Futures;
43import com.google.common.util.concurrent.ListenableFuture;
Chi Wang4ca89462021-06-10 04:49:49 -070044import com.google.common.util.concurrent.MoreExecutors;
buchgrff008f42018-06-02 14:13:43 -070045import com.google.common.util.concurrent.SettableFuture;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070046import com.google.devtools.build.lib.actions.ActionInput;
47import com.google.devtools.build.lib.actions.Artifact;
Googler04c546f2020-05-12 18:22:18 -070048import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
49import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080050import com.google.devtools.build.lib.actions.EnvironmentalExecException;
51import com.google.devtools.build.lib.actions.ExecException;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -070052import com.google.devtools.build.lib.actions.UserExecException;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070053import com.google.devtools.build.lib.actions.cache.MetadataInjector;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080054import com.google.devtools.build.lib.concurrent.ThreadSafety;
Chi Wang0f812eb2021-07-06 01:14:43 -070055import com.google.devtools.build.lib.exec.SpawnProgressEvent;
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -070056import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionContext;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070057import com.google.devtools.build.lib.profiler.Profiler;
58import com.google.devtools.build.lib.profiler.SilentCloseable;
Jakob Buchgraber60566092019-11-11 06:28:22 -080059import com.google.devtools.build.lib.remote.RemoteCache.ActionResultMetadata.DirectoryMetadata;
60import com.google.devtools.build.lib.remote.RemoteCache.ActionResultMetadata.FileMetadata;
61import com.google.devtools.build.lib.remote.RemoteCache.ActionResultMetadata.SymlinkMetadata;
Johannes Abtba5b2a72021-06-16 23:47:38 -070062import com.google.devtools.build.lib.remote.common.LazyFileOutputStream;
Chi Wang4ca89462021-06-10 04:49:49 -070063import com.google.devtools.build.lib.remote.common.OutputDigestMismatchException;
Chi Wang0f812eb2021-07-06 01:14:43 -070064import com.google.devtools.build.lib.remote.common.ProgressStatusListener;
Googlerbc54c642021-01-26 01:24:39 -080065import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext;
Chi Wangb6e3ba82021-01-14 21:57:28 -080066import com.google.devtools.build.lib.remote.common.RemoteActionFileArtifactValue;
Jakob Buchgraber60566092019-11-11 06:28:22 -080067import com.google.devtools.build.lib.remote.common.RemoteCacheClient;
68import com.google.devtools.build.lib.remote.common.RemoteCacheClient.ActionKey;
Chi Wangb863dbf2021-04-19 00:55:59 -070069import com.google.devtools.build.lib.remote.common.RemotePathResolver;
Jakob Buchgraber75b7ed42019-03-27 10:27:13 -070070import com.google.devtools.build.lib.remote.options.RemoteOptions;
Googler922d1e62018-03-05 14:49:00 -080071import com.google.devtools.build.lib.remote.util.DigestUtil;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070072import com.google.devtools.build.lib.remote.util.Utils.InMemoryOutput;
mschaller1eabf522020-06-10 22:03:13 -070073import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
74import com.google.devtools.build.lib.server.FailureDetails.RemoteExecution;
75import com.google.devtools.build.lib.server.FailureDetails.RemoteExecution.Code;
jhorvitzed7ec3b2020-07-24 15:08:03 -070076import com.google.devtools.build.lib.skyframe.TreeArtifactValue;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080077import com.google.devtools.build.lib.util.io.FileOutErr;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070078import com.google.devtools.build.lib.util.io.OutErr;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080079import com.google.devtools.build.lib.vfs.Dirent;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -070080import com.google.devtools.build.lib.vfs.FileStatus;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080081import com.google.devtools.build.lib.vfs.FileSystemUtils;
82import com.google.devtools.build.lib.vfs.Path;
olaolafbfb3cb2018-11-08 11:14:57 -080083import com.google.devtools.build.lib.vfs.PathFragment;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -070084import com.google.devtools.build.lib.vfs.Symlinks;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070085import com.google.protobuf.ByteString;
86import com.google.protobuf.InvalidProtocolBufferException;
buchgrff008f42018-06-02 14:13:43 -070087import java.io.ByteArrayOutputStream;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080088import java.io.IOException;
89import java.io.OutputStream;
90import java.util.ArrayList;
91import java.util.Collection;
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -070092import java.util.Collections;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080093import java.util.Comparator;
94import java.util.HashMap;
95import java.util.List;
96import java.util.Map;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070097import java.util.Map.Entry;
Chi Wang0f812eb2021-07-06 01:14:43 -070098import java.util.concurrent.atomic.AtomicLong;
99import java.util.regex.Matcher;
100import java.util.regex.Pattern;
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700101import java.util.stream.Collectors;
102import java.util.stream.Stream;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800103import javax.annotation.Nullable;
104
105/** A cache for storing artifacts (input and output) as well as the output of running an action. */
106@ThreadSafety.ThreadSafe
Jakob Buchgraber60566092019-11-11 06:28:22 -0800107public class RemoteCache implements AutoCloseable {
Chi Wangc7c7f5f2020-11-09 22:29:35 -0800108 private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
buchgrff008f42018-06-02 14:13:43 -0700109
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700110 /** See {@link SpawnExecutionContext#lockOutputFiles()}. */
111 @FunctionalInterface
112 interface OutputFilesLocker {
michajlo6bc145b2020-10-16 14:41:35 -0700113 void lock() throws InterruptedException;
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700114 }
115
jhorvitzed7ec3b2020-07-24 15:08:03 -0700116 private static final ListenableFuture<Void> COMPLETED_SUCCESS = immediateFuture(null);
117 private static final ListenableFuture<byte[]> EMPTY_BYTES = immediateFuture(new byte[0]);
buchgrff008f42018-06-02 14:13:43 -0700118
Jakob Buchgraber60566092019-11-11 06:28:22 -0800119 protected final RemoteCacheClient cacheProtocol;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700120 protected final RemoteOptions options;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800121 protected final DigestUtil digestUtil;
122
Chi Wang4ca89462021-06-10 04:49:49 -0700123 private Path captureCorruptedOutputsDir;
124
Jakob Buchgraber60566092019-11-11 06:28:22 -0800125 public RemoteCache(
126 RemoteCacheClient cacheProtocol, RemoteOptions options, DigestUtil digestUtil) {
127 this.cacheProtocol = cacheProtocol;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700128 this.options = options;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800129 this.digestUtil = digestUtil;
130 }
131
Chi Wang4ca89462021-06-10 04:49:49 -0700132 public void setCaptureCorruptedOutputsDir(Path captureCorruptedOutputsDir) {
133 this.captureCorruptedOutputsDir = captureCorruptedOutputsDir;
134 }
135
Googlerbc54c642021-01-26 01:24:39 -0800136 public ActionResult downloadActionResult(
137 RemoteActionExecutionContext context, ActionKey actionKey, boolean inlineOutErr)
Jakob Buchgraber60566092019-11-11 06:28:22 -0800138 throws IOException, InterruptedException {
Googlerbc54c642021-01-26 01:24:39 -0800139 return getFromFuture(cacheProtocol.downloadActionResult(context, actionKey, inlineOutErr));
Jakob Buchgraber60566092019-11-11 06:28:22 -0800140 }
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700141
buchgr12ebb842019-08-21 02:03:24 -0700142 /**
143 * Upload the result of a locally executed action to the remote cache.
144 *
145 * @throws IOException if there was an error uploading to the remote cache
146 * @throws ExecException if uploading any of the action outputs is not supported
147 */
148 public ActionResult upload(
Googler92955e62021-01-27 20:42:16 -0800149 RemoteActionExecutionContext context,
Chi Wangb863dbf2021-04-19 00:55:59 -0700150 RemotePathResolver remotePathResolver,
buchgr12ebb842019-08-21 02:03:24 -0700151 ActionKey actionKey,
152 Action action,
153 Command command,
buchgr12ebb842019-08-21 02:03:24 -0700154 Collection<Path> outputs,
155 FileOutErr outErr,
156 int exitCode)
157 throws ExecException, IOException, InterruptedException {
158 ActionResult.Builder resultBuilder = ActionResult.newBuilder();
Chi Wangb863dbf2021-04-19 00:55:59 -0700159 uploadOutputs(
160 context, remotePathResolver, actionKey, action, command, outputs, outErr, resultBuilder);
buchgr12ebb842019-08-21 02:03:24 -0700161 resultBuilder.setExitCode(exitCode);
162 ActionResult result = resultBuilder.build();
163 if (exitCode == 0 && !action.getDoNotCache()) {
Googler92955e62021-01-27 20:42:16 -0800164 cacheProtocol.uploadActionResult(context, actionKey, result);
buchgr12ebb842019-08-21 02:03:24 -0700165 }
166 return result;
167 }
168
169 public ActionResult upload(
Googler92955e62021-01-27 20:42:16 -0800170 RemoteActionExecutionContext context,
Chi Wangb863dbf2021-04-19 00:55:59 -0700171 RemotePathResolver remotePathResolver,
buchgr12ebb842019-08-21 02:03:24 -0700172 ActionKey actionKey,
173 Action action,
174 Command command,
buchgr12ebb842019-08-21 02:03:24 -0700175 Collection<Path> outputs,
176 FileOutErr outErr)
177 throws ExecException, IOException, InterruptedException {
Googler92955e62021-01-27 20:42:16 -0800178 return upload(
Chi Wangb863dbf2021-04-19 00:55:59 -0700179 context,
180 remotePathResolver,
181 actionKey,
182 action,
183 command,
184 outputs,
185 outErr,
186 /* exitCode= */ 0);
buchgr12ebb842019-08-21 02:03:24 -0700187 }
188
189 private void uploadOutputs(
Googler37ee2522021-01-28 22:54:20 -0800190 RemoteActionExecutionContext context,
Chi Wangb863dbf2021-04-19 00:55:59 -0700191 RemotePathResolver remotePathResolver,
buchgr12ebb842019-08-21 02:03:24 -0700192 ActionKey actionKey,
193 Action action,
194 Command command,
195 Collection<Path> files,
196 FileOutErr outErr,
197 ActionResult.Builder result)
198 throws ExecException, IOException, InterruptedException {
199 UploadManifest manifest =
200 new UploadManifest(
201 digestUtil,
Chi Wangb863dbf2021-04-19 00:55:59 -0700202 remotePathResolver,
buchgr12ebb842019-08-21 02:03:24 -0700203 result,
buchgr12ebb842019-08-21 02:03:24 -0700204 options.incompatibleRemoteSymlinks,
205 options.allowSymlinkUpload);
206 manifest.addFiles(files);
207 manifest.setStdoutStderr(outErr);
208 manifest.addAction(actionKey, action, command);
209
210 Map<Digest, Path> digestToFile = manifest.getDigestToFile();
211 Map<Digest, ByteString> digestToBlobs = manifest.getDigestToBlobs();
212 Collection<Digest> digests = new ArrayList<>();
213 digests.addAll(digestToFile.keySet());
214 digests.addAll(digestToBlobs.keySet());
215
Googlerf8d49fa2021-01-29 00:03:35 -0800216 ImmutableSet<Digest> digestsToUpload =
217 getFromFuture(cacheProtocol.findMissingDigests(context, digests));
buchgr12ebb842019-08-21 02:03:24 -0700218 ImmutableList.Builder<ListenableFuture<Void>> uploads = ImmutableList.builder();
219 for (Digest digest : digestsToUpload) {
220 Path file = digestToFile.get(digest);
221 if (file != null) {
Googler37ee2522021-01-28 22:54:20 -0800222 uploads.add(cacheProtocol.uploadFile(context, digest, file));
buchgr12ebb842019-08-21 02:03:24 -0700223 } else {
224 ByteString blob = digestToBlobs.get(digest);
225 if (blob == null) {
226 String message = "FindMissingBlobs call returned an unknown digest: " + digest;
227 throw new IOException(message);
228 }
Googler37ee2522021-01-28 22:54:20 -0800229 uploads.add(cacheProtocol.uploadBlob(context, digest, blob));
buchgr12ebb842019-08-21 02:03:24 -0700230 }
231 }
232
George Gensureaeee3e02020-04-15 04:43:45 -0700233 waitForBulkTransfer(uploads.build(), /* cancelRemainingOnInterrupt=*/ false);
buchgr12ebb842019-08-21 02:03:24 -0700234
235 if (manifest.getStderrDigest() != null) {
236 result.setStderrDigest(manifest.getStderrDigest());
237 }
238 if (manifest.getStdoutDigest() != null) {
239 result.setStdoutDigest(manifest.getStdoutDigest());
240 }
241 }
242
Ulf Adams4f008c52020-09-17 01:17:59 -0700243 public static void waitForBulkTransfer(
244 Iterable<? extends ListenableFuture<?>> transfers, boolean cancelRemainingOnInterrupt)
George Gensureaeee3e02020-04-15 04:43:45 -0700245 throws BulkTransferException, InterruptedException {
246 BulkTransferException bulkTransferException = null;
247 InterruptedException interruptedException = null;
248 boolean interrupted = Thread.currentThread().isInterrupted();
Ulf Adams4f008c52020-09-17 01:17:59 -0700249 for (ListenableFuture<?> transfer : transfers) {
George Gensureaeee3e02020-04-15 04:43:45 -0700250 try {
251 if (interruptedException == null) {
George Gensure24f97e12020-04-17 05:42:46 -0700252 // Wait for all transfers to finish.
Chi Wang62e169a2020-11-12 00:26:32 -0800253 getFromFuture(transfer, cancelRemainingOnInterrupt);
George Gensureaeee3e02020-04-15 04:43:45 -0700254 } else {
255 transfer.cancel(true);
256 }
257 } catch (IOException e) {
258 if (bulkTransferException == null) {
259 bulkTransferException = new BulkTransferException();
260 }
261 bulkTransferException.add(e);
262 } catch (InterruptedException e) {
263 interrupted = Thread.interrupted() || interrupted;
264 interruptedException = e;
265 if (!cancelRemainingOnInterrupt) {
266 // leave the rest of the transfers alone
267 break;
268 }
buchgr12ebb842019-08-21 02:03:24 -0700269 }
George Gensureaeee3e02020-04-15 04:43:45 -0700270 }
271 if (interrupted) {
272 Thread.currentThread().interrupt();
273 }
274 if (interruptedException != null) {
275 if (bulkTransferException != null) {
276 interruptedException.addSuppressed(bulkTransferException);
buchgr12ebb842019-08-21 02:03:24 -0700277 }
George Gensureaeee3e02020-04-15 04:43:45 -0700278 throw interruptedException;
279 }
280 if (bulkTransferException != null) {
281 throw bulkTransferException;
buchgr12ebb842019-08-21 02:03:24 -0700282 }
283 }
284
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700285 /**
buchgrff008f42018-06-02 14:13:43 -0700286 * Downloads a blob with content hash {@code digest} and stores its content in memory.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800287 *
buchgrff008f42018-06-02 14:13:43 -0700288 * @return a future that completes after the download completes (succeeds / fails). If successful,
289 * the content is stored in the future's {@code byte[]}.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800290 */
Googler75bd1ff2021-01-27 21:34:14 -0800291 public ListenableFuture<byte[]> downloadBlob(
292 RemoteActionExecutionContext context, Digest digest) {
buchgrff008f42018-06-02 14:13:43 -0700293 if (digest.getSizeBytes() == 0) {
294 return EMPTY_BYTES;
295 }
296 ByteArrayOutputStream bOut = new ByteArrayOutputStream((int) digest.getSizeBytes());
297 SettableFuture<byte[]> outerF = SettableFuture.create();
298 Futures.addCallback(
Googler75bd1ff2021-01-27 21:34:14 -0800299 cacheProtocol.downloadBlob(context, digest, bOut),
buchgrff008f42018-06-02 14:13:43 -0700300 new FutureCallback<Void>() {
301 @Override
302 public void onSuccess(Void aVoid) {
Chi Wangc7c7f5f2020-11-09 22:29:35 -0800303 try {
304 outerF.set(bOut.toByteArray());
305 } catch (RuntimeException e) {
306 logger.atWarning().withCause(e).log("Unexpected exception");
307 outerF.setException(e);
308 }
buchgrff008f42018-06-02 14:13:43 -0700309 }
310
311 @Override
312 public void onFailure(Throwable t) {
313 outerF.setException(t);
314 }
315 },
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700316 directExecutor());
buchgrff008f42018-06-02 14:13:43 -0700317 return outerF;
318 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800319
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700320 private static Path toTmpDownloadPath(Path actualPath) {
321 return actualPath.getParentDirectory().getRelative(actualPath.getBaseName() + ".tmp");
322 }
323
Chi Wang0f812eb2021-07-06 01:14:43 -0700324 static class DownloadProgressReporter {
325 private static final Pattern PATTERN = Pattern.compile("^bazel-out/[^/]+/[^/]+/");
326 private final ProgressStatusListener listener;
327 private final String id;
328 private final String file;
329 private final String totalSize;
330 private final AtomicLong downloadedBytes = new AtomicLong(0);
331
332 DownloadProgressReporter(ProgressStatusListener listener, String file, long totalSize) {
333 this.listener = listener;
334 this.id = file;
335 this.totalSize = bytesCountToDisplayString(totalSize);
336
337 Matcher matcher = PATTERN.matcher(file);
338 this.file = matcher.replaceFirst("");
339 }
340
341 void started() {
342 reportProgress(false, false);
343 }
344
345 void downloadedBytes(int count) {
346 downloadedBytes.addAndGet(count);
347 reportProgress(true, false);
348 }
349
350 void finished() {
351 reportProgress(true, true);
352 }
353
354 private void reportProgress(boolean includeBytes, boolean finished) {
355 String progress;
356 if (includeBytes) {
357 progress =
358 String.format(
359 "Downloading %s, %s / %s",
360 file, bytesCountToDisplayString(downloadedBytes.get()), totalSize);
361 } else {
362 progress = String.format("Downloading %s", file);
363 }
364 listener.onProgressStatus(SpawnProgressEvent.create(id, progress, finished));
365 }
366 }
367
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800368 /**
369 * Download the output files and directory trees of a remotely executed action to the local
370 * machine, as well stdin / stdout to the given files.
371 *
372 * <p>In case of failure, this method deletes any output files it might have already created.
373 *
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700374 * @param outputFilesLocker ensures that we are the only ones writing to the output files when
375 * using the dynamic spawn strategy.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800376 * @throws IOException in case of a cache miss or if the remote cache is unavailable.
377 * @throws ExecException in case clean up after a failed download failed.
378 */
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700379 public void download(
Googler75bd1ff2021-01-27 21:34:14 -0800380 RemoteActionExecutionContext context,
Chi Wangb863dbf2021-04-19 00:55:59 -0700381 RemotePathResolver remotePathResolver,
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700382 ActionResult result,
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700383 FileOutErr origOutErr,
Chi Wang0f812eb2021-07-06 01:14:43 -0700384 OutputFilesLocker outputFilesLocker,
385 ProgressStatusListener progressStatusListener)
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800386 throws ExecException, IOException, InterruptedException {
Chi Wangb863dbf2021-04-19 00:55:59 -0700387 ActionResultMetadata metadata = parseActionResultMetadata(context, remotePathResolver, result);
buchgrff008f42018-06-02 14:13:43 -0700388
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700389 List<ListenableFuture<FileMetadata>> downloads =
390 Stream.concat(
391 metadata.files().stream(),
392 metadata.directories().stream()
393 .flatMap((entry) -> entry.getValue().files().stream()))
394 .map(
395 (file) -> {
396 try {
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700397 ListenableFuture<Void> download =
Chi Wang4ca89462021-06-10 04:49:49 -0700398 downloadFile(
399 context,
400 remotePathResolver.localPathToOutputPath(file.path()),
401 toTmpDownloadPath(file.path()),
Chi Wang0f812eb2021-07-06 01:14:43 -0700402 file.digest(),
403 new DownloadProgressReporter(
404 progressStatusListener,
405 remotePathResolver.localPathToOutputPath(file.path()),
406 file.digest().getSizeBytes()));
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700407 return Futures.transform(download, (d) -> file, directExecutor());
408 } catch (IOException e) {
409 return Futures.<FileMetadata>immediateFailedFuture(e);
410 }
411 })
412 .collect(Collectors.toList());
buchgrff008f42018-06-02 14:13:43 -0700413
buchgrbca191282018-07-25 05:40:42 -0700414 // Subsequently we need to wait for *every* download to finish, even if we already know that
415 // one failed. That's so that when exiting this method we can be sure that all downloads have
416 // finished and don't race with the cleanup routine.
buchgrbca191282018-07-25 05:40:42 -0700417
pcloudy130f86d2019-04-29 05:55:23 -0700418 FileOutErr tmpOutErr = null;
George Gensureaeee3e02020-04-15 04:43:45 -0700419 if (origOutErr != null) {
420 tmpOutErr = origOutErr.childOutErr();
421 }
Googler75bd1ff2021-01-27 21:34:14 -0800422 downloads.addAll(downloadOutErr(context, result, tmpOutErr));
George Gensureaeee3e02020-04-15 04:43:45 -0700423
buchgrbca191282018-07-25 05:40:42 -0700424 try {
George Gensureaeee3e02020-04-15 04:43:45 -0700425 waitForBulkTransfer(downloads, /* cancelRemainingOnInterrupt=*/ true);
426 } catch (Exception e) {
Chi Wang4ca89462021-06-10 04:49:49 -0700427 if (captureCorruptedOutputsDir != null) {
428 if (e instanceof BulkTransferException) {
429 for (Throwable suppressed : e.getSuppressed()) {
430 if (suppressed instanceof OutputDigestMismatchException) {
431 // Capture corrupted outputs
432 try {
433 String outputPath = ((OutputDigestMismatchException) suppressed).getOutputPath();
434 Path localPath = ((OutputDigestMismatchException) suppressed).getLocalPath();
435 Path dst = captureCorruptedOutputsDir.getRelative(outputPath);
436 dst.createDirectoryAndParents();
437
438 // Make sure dst is still under captureCorruptedOutputsDir, otherwise
439 // IllegalArgumentException will be thrown.
440 dst.relativeTo(captureCorruptedOutputsDir);
441
442 FileSystemUtils.copyFile(localPath, dst);
443 } catch (Exception ee) {
444 ee.addSuppressed(ee);
445 }
446 }
447 }
448 }
449 }
450
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800451 try {
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700452 // Delete any (partially) downloaded output files.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800453 for (OutputFile file : result.getOutputFilesList()) {
Chi Wangb863dbf2021-04-19 00:55:59 -0700454 toTmpDownloadPath(remotePathResolver.outputPathToLocalPath(file.getPath())).delete();
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800455 }
456 for (OutputDirectory directory : result.getOutputDirectoriesList()) {
Keith Smiley4392ba42018-12-10 02:39:37 -0800457 // Only delete the directories below the output directories because the output
458 // directories will not be re-created
Chi Wangb863dbf2021-04-19 00:55:59 -0700459 remotePathResolver.outputPathToLocalPath(directory.getPath()).deleteTreesBelow();
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800460 }
pcloudy130f86d2019-04-29 05:55:23 -0700461 if (tmpOutErr != null) {
462 tmpOutErr.clearOut();
463 tmpOutErr.clearErr();
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800464 }
George Gensureaeee3e02020-04-15 04:43:45 -0700465 } catch (IOException ioEx) {
466 ioEx.addSuppressed(e);
Jakob Buchgraber4dc78a02019-09-26 07:53:11 -0700467
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800468 // If deleting of output files failed, we abort the build with a decent error message as
469 // any subsequent local execution failure would likely be incomprehensible.
George Gensureaeee3e02020-04-15 04:43:45 -0700470 ExecException execEx =
471 new EnvironmentalExecException(
mschaller1eabf522020-06-10 22:03:13 -0700472 ioEx,
mschaller07933882020-06-24 14:38:23 -0700473 createFailureDetail(
474 "Failed to delete output files after incomplete download",
475 Code.INCOMPLETE_OUTPUT_DOWNLOAD_CLEANUP_FAILURE));
George Gensureaeee3e02020-04-15 04:43:45 -0700476 execEx.addSuppressed(e);
477 throw execEx;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800478 }
George Gensureaeee3e02020-04-15 04:43:45 -0700479 throw e;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800480 }
olaolafbfb3cb2018-11-08 11:14:57 -0800481
pcloudy130f86d2019-04-29 05:55:23 -0700482 if (tmpOutErr != null) {
483 FileOutErr.dump(tmpOutErr, origOutErr);
484 tmpOutErr.clearOut();
485 tmpOutErr.clearErr();
486 }
487
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700488 // Ensure that we are the only ones writing to the output files when using the dynamic spawn
489 // strategy.
490 outputFilesLocker.lock();
491
492 moveOutputsToFinalLocation(downloads);
493
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700494 List<SymlinkMetadata> symlinksInDirectories = new ArrayList<>();
495 for (Entry<Path, DirectoryMetadata> entry : metadata.directories()) {
496 entry.getKey().createDirectoryAndParents();
497 symlinksInDirectories.addAll(entry.getValue().symlinks());
498 }
499
500 Iterable<SymlinkMetadata> symlinks =
501 Iterables.concat(metadata.symlinks(), symlinksInDirectories);
502
503 // Create the symbolic links after all downloads are finished, because dangling symlinks
504 // might not be supported on all platforms
505 createSymlinks(symlinks);
olaolafbfb3cb2018-11-08 11:14:57 -0800506 }
507
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700508 /**
509 * Copies moves the downloaded outputs from their download location to their declared location.
510 */
511 private void moveOutputsToFinalLocation(List<ListenableFuture<FileMetadata>> downloads)
512 throws IOException, InterruptedException {
513 List<FileMetadata> finishedDownloads = new ArrayList<>(downloads.size());
514 for (ListenableFuture<FileMetadata> finishedDownload : downloads) {
515 FileMetadata outputFile = getFromFuture(finishedDownload);
516 if (outputFile != null) {
517 finishedDownloads.add(outputFile);
518 }
519 }
520 /*
521 * Sort the list lexicographically based on its temporary download path in order to avoid
522 * filename clashes when moving the files:
523 *
524 * Consider an action that produces two outputs foo and foo.tmp. These outputs would initially
525 * be downloaded to foo.tmp and foo.tmp.tmp. When renaming them to foo and foo.tmp we need to
526 * ensure that rename(foo.tmp, foo) happens before rename(foo.tmp.tmp, foo.tmp). We ensure this
527 * by doing the renames in lexicographical order of the download names.
528 */
529 Collections.sort(finishedDownloads, Comparator.comparing(f -> toTmpDownloadPath(f.path())));
530
531 // Move the output files from their temporary name to the actual output file name.
532 for (FileMetadata outputFile : finishedDownloads) {
533 FileSystemUtils.moveFile(toTmpDownloadPath(outputFile.path()), outputFile.path());
534 outputFile.path().setExecutable(outputFile.isExecutable());
535 }
536 }
537
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700538 private void createSymlinks(Iterable<SymlinkMetadata> symlinks) throws IOException {
539 for (SymlinkMetadata symlink : symlinks) {
540 if (symlink.target().isAbsolute()) {
541 // We do not support absolute symlinks as outputs.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800542 throw new IOException(
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700543 String.format(
544 "Action output %s is a symbolic link to an absolute path %s. "
545 + "Symlinks to absolute paths in action outputs are not supported.",
546 symlink.path(), symlink.target()));
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800547 }
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700548 Preconditions.checkNotNull(
549 symlink.path().getParentDirectory(),
550 "Failed creating directory and parents for %s",
551 symlink.path())
552 .createDirectoryAndParents();
553 symlink.path().createSymbolicLink(symlink.target());
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800554 }
555 }
556
Chi Wang4ca89462021-06-10 04:49:49 -0700557 public ListenableFuture<Void> downloadFile(
Chi Wang0f812eb2021-07-06 01:14:43 -0700558 RemoteActionExecutionContext context,
559 String outputPath,
560 Path localPath,
561 Digest digest,
562 DownloadProgressReporter reporter)
Chi Wang4ca89462021-06-10 04:49:49 -0700563 throws IOException {
564 SettableFuture<Void> outerF = SettableFuture.create();
Chi Wang0f812eb2021-07-06 01:14:43 -0700565 ListenableFuture<Void> f = downloadFile(context, localPath, digest, reporter);
Chi Wang4ca89462021-06-10 04:49:49 -0700566 Futures.addCallback(
567 f,
568 new FutureCallback<Void>() {
569 @Override
570 public void onSuccess(Void unused) {
571 outerF.set(null);
572 }
573
574 @Override
575 public void onFailure(Throwable throwable) {
576 if (throwable instanceof OutputDigestMismatchException) {
577 OutputDigestMismatchException e = ((OutputDigestMismatchException) throwable);
578 e.setOutputPath(outputPath);
579 e.setLocalPath(localPath);
580 }
581 outerF.setException(throwable);
582 }
583 },
584 MoreExecutors.directExecutor());
585
586 return outerF;
587 }
588
jhorvitzed7ec3b2020-07-24 15:08:03 -0700589 /** Downloads a file (that is not a directory). The content is fetched from the digest. */
Googler75bd1ff2021-01-27 21:34:14 -0800590 public ListenableFuture<Void> downloadFile(
591 RemoteActionExecutionContext context, Path path, Digest digest) throws IOException {
Chi Wang0f812eb2021-07-06 01:14:43 -0700592 return downloadFile(context, path, digest, new DownloadProgressReporter(NO_ACTION, "", 0));
593 }
594
595 /** Downloads a file (that is not a directory). The content is fetched from the digest. */
596 public ListenableFuture<Void> downloadFile(
597 RemoteActionExecutionContext context,
598 Path path,
599 Digest digest,
600 DownloadProgressReporter reporter)
601 throws IOException {
buchgrff008f42018-06-02 14:13:43 -0700602 Preconditions.checkNotNull(path.getParentDirectory()).createDirectoryAndParents();
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800603 if (digest.getSizeBytes() == 0) {
604 // Handle empty file locally.
605 FileSystemUtils.writeContent(path, new byte[0]);
buchgrff008f42018-06-02 14:13:43 -0700606 return COMPLETED_SUCCESS;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800607 }
buchgrff008f42018-06-02 14:13:43 -0700608
Ed Schoutencb08ffc2020-09-09 23:32:01 -0700609 if (!options.remoteDownloadSymlinkTemplate.isEmpty()) {
610 // Don't actually download files from the CAS. Instead, create a
611 // symbolic link that points to a location where CAS objects may
612 // be found. This could, for example, be a FUSE file system.
613 path.createSymbolicLink(
614 path.getRelative(
615 options
616 .remoteDownloadSymlinkTemplate
617 .replace("{hash}", digest.getHash())
618 .replace("{size_bytes}", String.valueOf(digest.getSizeBytes()))));
619 return COMPLETED_SUCCESS;
620 }
621
Chi Wang0f812eb2021-07-06 01:14:43 -0700622 reporter.started();
623 OutputStream out = new ReportingOutputStream(new LazyFileOutputStream(path), reporter);
624
buchgrff008f42018-06-02 14:13:43 -0700625 SettableFuture<Void> outerF = SettableFuture.create();
Googler75bd1ff2021-01-27 21:34:14 -0800626 ListenableFuture<Void> f = cacheProtocol.downloadBlob(context, digest, out);
buchgrff008f42018-06-02 14:13:43 -0700627 Futures.addCallback(
628 f,
629 new FutureCallback<Void>() {
630 @Override
631 public void onSuccess(Void result) {
632 try {
633 out.close();
634 outerF.set(null);
Chi Wang0f812eb2021-07-06 01:14:43 -0700635 reporter.finished();
buchgrff008f42018-06-02 14:13:43 -0700636 } catch (IOException e) {
637 outerF.setException(e);
Chi Wangc7c7f5f2020-11-09 22:29:35 -0800638 } catch (RuntimeException e) {
639 logger.atWarning().withCause(e).log("Unexpected exception");
640 outerF.setException(e);
buchgrff008f42018-06-02 14:13:43 -0700641 }
642 }
643
644 @Override
645 public void onFailure(Throwable t) {
buchgrff008f42018-06-02 14:13:43 -0700646 try {
647 out.close();
Chi Wang0f812eb2021-07-06 01:14:43 -0700648 reporter.finished();
buchgrff008f42018-06-02 14:13:43 -0700649 } catch (IOException e) {
Jakob Buchgraber4dc78a02019-09-26 07:53:11 -0700650 if (t != e) {
651 t.addSuppressed(e);
652 }
Chi Wangc7c7f5f2020-11-09 22:29:35 -0800653 } catch (RuntimeException e) {
654 logger.atWarning().withCause(e).log("Unexpected exception");
655 t.addSuppressed(e);
pcloudya3a59752019-01-07 03:48:37 -0800656 } finally {
657 outerF.setException(t);
buchgrff008f42018-06-02 14:13:43 -0700658 }
659 }
660 },
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700661 directExecutor());
buchgrff008f42018-06-02 14:13:43 -0700662 return outerF;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800663 }
664
Googler75bd1ff2021-01-27 21:34:14 -0800665 private List<ListenableFuture<FileMetadata>> downloadOutErr(
666 RemoteActionExecutionContext context, ActionResult result, OutErr outErr) {
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700667 List<ListenableFuture<FileMetadata>> downloads = new ArrayList<>();
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800668 if (!result.getStdoutRaw().isEmpty()) {
George Gensureaeee3e02020-04-15 04:43:45 -0700669 try {
670 result.getStdoutRaw().writeTo(outErr.getOutputStream());
671 outErr.getOutputStream().flush();
672 } catch (IOException e) {
673 downloads.add(Futures.immediateFailedFuture(e));
674 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800675 } else if (result.hasStdoutDigest()) {
buchgrff008f42018-06-02 14:13:43 -0700676 downloads.add(
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700677 Futures.transform(
Googler75bd1ff2021-01-27 21:34:14 -0800678 cacheProtocol.downloadBlob(
679 context, result.getStdoutDigest(), outErr.getOutputStream()),
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700680 (d) -> null,
681 directExecutor()));
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800682 }
683 if (!result.getStderrRaw().isEmpty()) {
George Gensureaeee3e02020-04-15 04:43:45 -0700684 try {
685 result.getStderrRaw().writeTo(outErr.getErrorStream());
686 outErr.getErrorStream().flush();
687 } catch (IOException e) {
688 downloads.add(Futures.immediateFailedFuture(e));
689 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800690 } else if (result.hasStderrDigest()) {
buchgrff008f42018-06-02 14:13:43 -0700691 downloads.add(
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700692 Futures.transform(
Googler75bd1ff2021-01-27 21:34:14 -0800693 cacheProtocol.downloadBlob(
694 context, result.getStderrDigest(), outErr.getErrorStream()),
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700695 (d) -> null,
696 directExecutor()));
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800697 }
buchgrff008f42018-06-02 14:13:43 -0700698 return downloads;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800699 }
700
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700701 /**
702 * Avoids downloading the majority of action outputs but injects their metadata using {@link
703 * MetadataInjector} instead.
704 *
705 * <p>This method only downloads output directory metadata, stdout and stderr as well as the
706 * contents of {@code inMemoryOutputPath} if specified.
707 *
Googler75bd1ff2021-01-27 21:34:14 -0800708 * @param context the context this action running with
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700709 * @param result the action result metadata of a successfully executed action (exit code = 0).
710 * @param outputs the action's declared output files
711 * @param inMemoryOutputPath the path of an output file whose contents should be returned in
712 * memory by this method.
713 * @param outErr stdout and stderr of this action
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700714 * @param metadataInjector the action's metadata injector that allows this method to inject
715 * metadata about an action output instead of downloading the output
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700716 * @param outputFilesLocker ensures that we are the only ones writing to the output files when
717 * using the dynamic spawn strategy.
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700718 * @throws IOException in case of failure
719 * @throws InterruptedException in case of receiving an interrupt
720 */
721 @Nullable
722 public InMemoryOutput downloadMinimal(
Googler75bd1ff2021-01-27 21:34:14 -0800723 RemoteActionExecutionContext context,
Chi Wangb863dbf2021-04-19 00:55:59 -0700724 RemotePathResolver remotePathResolver,
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700725 ActionResult result,
726 Collection<? extends ActionInput> outputs,
727 @Nullable PathFragment inMemoryOutputPath,
728 OutErr outErr,
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700729 MetadataInjector metadataInjector,
730 OutputFilesLocker outputFilesLocker)
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700731 throws IOException, InterruptedException {
732 Preconditions.checkState(
733 result.getExitCode() == 0,
734 "injecting remote metadata is only supported for successful actions (exit code 0).");
735
736 ActionResultMetadata metadata;
737 try (SilentCloseable c = Profiler.instance().profile("Remote.parseActionResultMetadata")) {
Chi Wangb863dbf2021-04-19 00:55:59 -0700738 metadata = parseActionResultMetadata(context, remotePathResolver, result);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700739 }
740
741 if (!metadata.symlinks().isEmpty()) {
742 throw new IOException(
743 "Symlinks in action outputs are not yet supported by "
buchgrd480c5f2019-04-03 00:53:34 -0700744 + "--experimental_remote_download_outputs=minimal");
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700745 }
746
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700747 // Ensure that when using dynamic spawn strategy that we are the only ones writing to the
748 // output files.
749 outputFilesLocker.lock();
750
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700751 ActionInput inMemoryOutput = null;
752 Digest inMemoryOutputDigest = null;
753 for (ActionInput output : outputs) {
754 if (inMemoryOutputPath != null && output.getExecPath().equals(inMemoryOutputPath)) {
Chi Wangb863dbf2021-04-19 00:55:59 -0700755 Path localPath = remotePathResolver.outputPathToLocalPath(output);
756 FileMetadata m = metadata.file(localPath);
Jakob Buchgrabere05dca32020-04-28 04:29:27 -0700757 if (m == null) {
758 // A declared output wasn't created. Ignore it here. SkyFrame will fail if not all
759 // outputs were created.
760 continue;
761 }
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700762 inMemoryOutputDigest = m.digest();
763 inMemoryOutput = output;
764 }
765 if (output instanceof Artifact) {
Chi Wangb863dbf2021-04-19 00:55:59 -0700766 injectRemoteArtifact(
767 context, remotePathResolver, (Artifact) output, metadata, metadataInjector);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700768 }
769 }
770
771 try (SilentCloseable c = Profiler.instance().profile("Remote.download")) {
772 ListenableFuture<byte[]> inMemoryOutputDownload = null;
773 if (inMemoryOutput != null) {
Googler75bd1ff2021-01-27 21:34:14 -0800774 inMemoryOutputDownload = downloadBlob(context, inMemoryOutputDigest);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700775 }
Googler75bd1ff2021-01-27 21:34:14 -0800776 waitForBulkTransfer(
777 downloadOutErr(context, result, outErr), /* cancelRemainingOnInterrupt=*/ true);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700778 if (inMemoryOutputDownload != null) {
George Gensure7cf5a5e2020-04-28 08:48:19 -0700779 waitForBulkTransfer(
780 ImmutableList.of(inMemoryOutputDownload), /* cancelRemainingOnInterrupt=*/ true);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700781 byte[] data = getFromFuture(inMemoryOutputDownload);
782 return new InMemoryOutput(inMemoryOutput, ByteString.copyFrom(data));
783 }
784 }
785 return null;
786 }
787
788 private void injectRemoteArtifact(
Chi Wangb863dbf2021-04-19 00:55:59 -0700789 RemoteActionExecutionContext context,
790 RemotePathResolver remotePathResolver,
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700791 Artifact output,
792 ActionResultMetadata metadata,
Chi Wangb863dbf2021-04-19 00:55:59 -0700793 MetadataInjector metadataInjector)
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700794 throws IOException {
Chi Wangb863dbf2021-04-19 00:55:59 -0700795 Path path = remotePathResolver.outputPathToLocalPath(output);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700796 if (output.isTreeArtifact()) {
Chi Wangb863dbf2021-04-19 00:55:59 -0700797 DirectoryMetadata directory = metadata.directory(path);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700798 if (directory == null) {
799 // A declared output wasn't created. It might have been an optional output and if not
800 // SkyFrame will make sure to fail.
801 return;
802 }
803 if (!directory.symlinks().isEmpty()) {
804 throw new IOException(
805 "Symlinks in action outputs are not yet supported by "
buchgrd480c5f2019-04-03 00:53:34 -0700806 + "--experimental_remote_download_outputs=minimal");
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700807 }
Googler04c546f2020-05-12 18:22:18 -0700808 SpecialArtifact parent = (SpecialArtifact) output;
jhorvitzed7ec3b2020-07-24 15:08:03 -0700809 TreeArtifactValue.Builder tree = TreeArtifactValue.newBuilder(parent);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700810 for (FileMetadata file : directory.files()) {
Googler04c546f2020-05-12 18:22:18 -0700811 TreeFileArtifact child =
Googler1d8d1382020-05-18 12:10:49 -0700812 TreeFileArtifact.createTreeOutput(parent, file.path().relativeTo(parent.getPath()));
Chi Wangb6e3ba82021-01-14 21:57:28 -0800813 RemoteActionFileArtifactValue value =
814 new RemoteActionFileArtifactValue(
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700815 DigestUtil.toBinaryDigest(file.digest()),
816 file.digest().getSizeBytes(),
Googler04c546f2020-05-12 18:22:18 -0700817 /*locationIndex=*/ 1,
Chi Wangb863dbf2021-04-19 00:55:59 -0700818 context.getRequestMetadata().getActionId(),
Chi Wangb6e3ba82021-01-14 21:57:28 -0800819 file.isExecutable());
jhorvitzed7ec3b2020-07-24 15:08:03 -0700820 tree.putChild(child, value);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700821 }
jhorvitzed7ec3b2020-07-24 15:08:03 -0700822 metadataInjector.injectTree(parent, tree.build());
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700823 } else {
Chi Wangb863dbf2021-04-19 00:55:59 -0700824 FileMetadata outputMetadata = metadata.file(path);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700825 if (outputMetadata == null) {
826 // A declared output wasn't created. It might have been an optional output and if not
827 // SkyFrame will make sure to fail.
828 return;
829 }
Googleref554462020-06-04 17:40:55 -0700830 metadataInjector.injectFile(
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700831 output,
Chi Wangb6e3ba82021-01-14 21:57:28 -0800832 new RemoteActionFileArtifactValue(
Googlerb67aaba2020-05-13 09:49:37 -0700833 DigestUtil.toBinaryDigest(outputMetadata.digest()),
834 outputMetadata.digest().getSizeBytes(),
835 /*locationIndex=*/ 1,
Chi Wangb863dbf2021-04-19 00:55:59 -0700836 context.getRequestMetadata().getActionId(),
Chi Wangb6e3ba82021-01-14 21:57:28 -0800837 outputMetadata.isExecutable()));
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700838 }
839 }
840
841 private DirectoryMetadata parseDirectory(
842 Path parent, Directory dir, Map<Digest, Directory> childDirectoriesMap) {
843 ImmutableList.Builder<FileMetadata> filesBuilder = ImmutableList.builder();
844 for (FileNode file : dir.getFilesList()) {
845 filesBuilder.add(
846 new FileMetadata(
847 parent.getRelative(file.getName()), file.getDigest(), file.getIsExecutable()));
848 }
849
850 ImmutableList.Builder<SymlinkMetadata> symlinksBuilder = ImmutableList.builder();
851 for (SymlinkNode symlink : dir.getSymlinksList()) {
852 symlinksBuilder.add(
853 new SymlinkMetadata(
854 parent.getRelative(symlink.getName()), PathFragment.create(symlink.getTarget())));
855 }
856
857 for (DirectoryNode directoryNode : dir.getDirectoriesList()) {
858 Path childPath = parent.getRelative(directoryNode.getName());
859 Directory childDir =
860 Preconditions.checkNotNull(childDirectoriesMap.get(directoryNode.getDigest()));
861 DirectoryMetadata childMetadata = parseDirectory(childPath, childDir, childDirectoriesMap);
862 filesBuilder.addAll(childMetadata.files());
863 symlinksBuilder.addAll(childMetadata.symlinks());
864 }
865
866 return new DirectoryMetadata(filesBuilder.build(), symlinksBuilder.build());
867 }
868
Googler75bd1ff2021-01-27 21:34:14 -0800869 private ActionResultMetadata parseActionResultMetadata(
Chi Wangb863dbf2021-04-19 00:55:59 -0700870 RemoteActionExecutionContext context,
871 RemotePathResolver remotePathResolver,
872 ActionResult actionResult)
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700873 throws IOException, InterruptedException {
874 Preconditions.checkNotNull(actionResult, "actionResult");
875 Map<Path, ListenableFuture<Tree>> dirMetadataDownloads =
876 Maps.newHashMapWithExpectedSize(actionResult.getOutputDirectoriesCount());
877 for (OutputDirectory dir : actionResult.getOutputDirectoriesList()) {
878 dirMetadataDownloads.put(
Chi Wangb863dbf2021-04-19 00:55:59 -0700879 remotePathResolver.outputPathToLocalPath(dir.getPath()),
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700880 Futures.transform(
Googler75bd1ff2021-01-27 21:34:14 -0800881 downloadBlob(context, dir.getTreeDigest()),
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700882 (treeBytes) -> {
883 try {
884 return Tree.parseFrom(treeBytes);
885 } catch (InvalidProtocolBufferException e) {
886 throw new RuntimeException(e);
887 }
888 },
889 directExecutor()));
890 }
891
George Gensure24f97e12020-04-17 05:42:46 -0700892 waitForBulkTransfer(dirMetadataDownloads.values(), /* cancelRemainingOnInterrupt=*/ true);
893
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700894 ImmutableMap.Builder<Path, DirectoryMetadata> directories = ImmutableMap.builder();
895 for (Map.Entry<Path, ListenableFuture<Tree>> metadataDownload :
896 dirMetadataDownloads.entrySet()) {
897 Path path = metadataDownload.getKey();
898 Tree directoryTree = getFromFuture(metadataDownload.getValue());
899 Map<Digest, Directory> childrenMap = new HashMap<>();
900 for (Directory childDir : directoryTree.getChildrenList()) {
901 childrenMap.put(digestUtil.compute(childDir), childDir);
902 }
903
904 directories.put(path, parseDirectory(path, directoryTree.getRoot(), childrenMap));
905 }
906
907 ImmutableMap.Builder<Path, FileMetadata> files = ImmutableMap.builder();
908 for (OutputFile outputFile : actionResult.getOutputFilesList()) {
Chi Wangb863dbf2021-04-19 00:55:59 -0700909 Path localPath = remotePathResolver.outputPathToLocalPath(outputFile.getPath());
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700910 files.put(
Chi Wangb863dbf2021-04-19 00:55:59 -0700911 localPath,
912 new FileMetadata(localPath, outputFile.getDigest(), outputFile.getIsExecutable()));
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700913 }
914
915 ImmutableMap.Builder<Path, SymlinkMetadata> symlinks = ImmutableMap.builder();
916 Iterable<OutputSymlink> outputSymlinks =
917 Iterables.concat(
918 actionResult.getOutputFileSymlinksList(),
919 actionResult.getOutputDirectorySymlinksList());
920 for (OutputSymlink symlink : outputSymlinks) {
Chi Wangb863dbf2021-04-19 00:55:59 -0700921 Path localPath = remotePathResolver.outputPathToLocalPath(symlink.getPath());
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700922 symlinks.put(
Chi Wangb863dbf2021-04-19 00:55:59 -0700923 localPath, new SymlinkMetadata(localPath, PathFragment.create(symlink.getTarget())));
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700924 }
925
926 return new ActionResultMetadata(files.build(), symlinks.build(), directories.build());
927 }
928
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700929 /** UploadManifest adds output metadata to a {@link ActionResult}. */
930 static class UploadManifest {
931 private final DigestUtil digestUtil;
Chi Wangb863dbf2021-04-19 00:55:59 -0700932 private final RemotePathResolver remotePathResolver;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800933 private final ActionResult.Builder result;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700934 private final boolean allowSymlinks;
olaolafbfb3cb2018-11-08 11:14:57 -0800935 private final boolean uploadSymlinks;
buchgr59b989e2019-08-06 01:36:23 -0700936 private final Map<Digest, Path> digestToFile = new HashMap<>();
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700937 private final Map<Digest, ByteString> digestToBlobs = new HashMap<>();
buchgr59b989e2019-08-06 01:36:23 -0700938 private Digest stderrDigest;
939 private Digest stdoutDigest;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800940
941 /**
942 * Create an UploadManifest from an ActionResult builder and an exec root. The ActionResult
943 * builder is populated through a call to {@link #addFile(Digest, Path)}.
944 */
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700945 public UploadManifest(
olaolafbfb3cb2018-11-08 11:14:57 -0800946 DigestUtil digestUtil,
Chi Wangb863dbf2021-04-19 00:55:59 -0700947 RemotePathResolver remotePathResolver,
olaolafbfb3cb2018-11-08 11:14:57 -0800948 ActionResult.Builder result,
olaolafbfb3cb2018-11-08 11:14:57 -0800949 boolean uploadSymlinks,
950 boolean allowSymlinks) {
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700951 this.digestUtil = digestUtil;
Chi Wangb863dbf2021-04-19 00:55:59 -0700952 this.remotePathResolver = remotePathResolver;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800953 this.result = result;
olaolafbfb3cb2018-11-08 11:14:57 -0800954 this.uploadSymlinks = uploadSymlinks;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700955 this.allowSymlinks = allowSymlinks;
buchgr59b989e2019-08-06 01:36:23 -0700956 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800957
buchgr59b989e2019-08-06 01:36:23 -0700958 public void setStdoutStderr(FileOutErr outErr) throws IOException {
959 if (outErr.getErrorPath().exists()) {
960 stderrDigest = digestUtil.compute(outErr.getErrorPath());
Jakob Buchgraberb7d300c2019-08-06 04:25:24 -0700961 digestToFile.put(stderrDigest, outErr.getErrorPath());
buchgr59b989e2019-08-06 01:36:23 -0700962 }
963 if (outErr.getOutputPath().exists()) {
964 stdoutDigest = digestUtil.compute(outErr.getOutputPath());
Jakob Buchgraberb7d300c2019-08-06 04:25:24 -0700965 digestToFile.put(stdoutDigest, outErr.getOutputPath());
buchgr59b989e2019-08-06 01:36:23 -0700966 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800967 }
968
969 /**
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700970 * Add a collection of files or directories to the UploadManifest. Adding a directory has the
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800971 * effect of 1) uploading a {@link Tree} protobuf message from which the whole structure of the
972 * directory, including the descendants, can be reconstructed and 2) uploading all the
973 * non-directory descendant files.
974 */
buchgrff008f42018-06-02 14:13:43 -0700975 public void addFiles(Collection<Path> files) throws ExecException, IOException {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800976 for (Path file : files) {
977 // TODO(ulfjack): Maybe pass in a SpawnResult here, add a list of output files to that, and
978 // rely on the local spawn runner to stat the files, instead of statting here.
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700979 FileStatus stat = file.statIfFound(Symlinks.NOFOLLOW);
olaolafbfb3cb2018-11-08 11:14:57 -0800980 // TODO(#6547): handle the case where the parent directory of the output file is an
981 // output symlink.
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700982 if (stat == null) {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800983 // We ignore requested results that have not been generated by the action.
984 continue;
985 }
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700986 if (stat.isDirectory()) {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800987 addDirectory(file);
olaolafbfb3cb2018-11-08 11:14:57 -0800988 } else if (stat.isFile() && !stat.isSpecialFile()) {
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700989 Digest digest = digestUtil.compute(file, stat.getSize());
buchgrfa36d2f2018-04-18 04:41:10 -0700990 addFile(digest, file);
olaolafbfb3cb2018-11-08 11:14:57 -0800991 } else if (stat.isSymbolicLink() && allowSymlinks) {
992 PathFragment target = file.readSymbolicLink();
993 // Need to resolve the symbolic link to know what to add, file or directory.
994 FileStatus statFollow = file.statIfFound(Symlinks.FOLLOW);
995 if (statFollow == null) {
996 throw new IOException(
997 String.format("Action output %s is a dangling symbolic link to %s ", file, target));
998 }
999 if (statFollow.isSpecialFile()) {
1000 illegalOutput(file);
1001 }
1002 Preconditions.checkState(
1003 statFollow.isFile() || statFollow.isDirectory(), "Unknown stat type for %s", file);
1004 if (uploadSymlinks && !target.isAbsolute()) {
1005 if (statFollow.isFile()) {
1006 addFileSymbolicLink(file, target);
1007 } else {
1008 addDirectorySymbolicLink(file, target);
1009 }
1010 } else {
1011 if (statFollow.isFile()) {
1012 addFile(digestUtil.compute(file), file);
1013 } else {
1014 addDirectory(file);
1015 }
1016 }
Benjamin Petersondd3ddb02018-05-03 09:20:08 -07001017 } else {
1018 illegalOutput(file);
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001019 }
1020 }
1021 }
1022
olaolaf0aa55d2018-08-16 08:51:06 -07001023 /**
1024 * Adds an action and command protos to upload. They need to be uploaded as part of the action
1025 * result.
1026 */
Jakob Buchgraber60566092019-11-11 06:28:22 -08001027 public void addAction(RemoteCacheClient.ActionKey actionKey, Action action, Command command) {
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -07001028 digestToBlobs.put(actionKey.getDigest(), action.toByteString());
1029 digestToBlobs.put(action.getCommandDigest(), command.toByteString());
olaolaf0aa55d2018-08-16 08:51:06 -07001030 }
1031
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001032 /** Map of digests to file paths to upload. */
1033 public Map<Digest, Path> getDigestToFile() {
1034 return digestToFile;
1035 }
1036
1037 /**
1038 * Map of digests to chunkers to upload. When the file is a regular, non-directory file it is
1039 * transmitted through {@link #getDigestToFile()}. When it is a directory, it is transmitted as
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -07001040 * a {@link Tree} protobuf message through {@link #getDigestToBlobs()}.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001041 */
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -07001042 public Map<Digest, ByteString> getDigestToBlobs() {
1043 return digestToBlobs;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001044 }
1045
buchgr59b989e2019-08-06 01:36:23 -07001046 @Nullable
1047 public Digest getStdoutDigest() {
1048 return stdoutDigest;
1049 }
1050
1051 @Nullable
1052 public Digest getStderrDigest() {
1053 return stderrDigest;
1054 }
1055
olaolafbfb3cb2018-11-08 11:14:57 -08001056 private void addFileSymbolicLink(Path file, PathFragment target) throws IOException {
1057 result
1058 .addOutputFileSymlinksBuilder()
Chi Wangb863dbf2021-04-19 00:55:59 -07001059 .setPath(remotePathResolver.localPathToOutputPath(file))
olaolafbfb3cb2018-11-08 11:14:57 -08001060 .setTarget(target.toString());
1061 }
1062
1063 private void addDirectorySymbolicLink(Path file, PathFragment target) throws IOException {
1064 result
1065 .addOutputDirectorySymlinksBuilder()
Chi Wangb863dbf2021-04-19 00:55:59 -07001066 .setPath(remotePathResolver.localPathToOutputPath(file))
olaolafbfb3cb2018-11-08 11:14:57 -08001067 .setTarget(target.toString());
1068 }
1069
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001070 private void addFile(Digest digest, Path file) throws IOException {
1071 result
1072 .addOutputFilesBuilder()
Chi Wangb863dbf2021-04-19 00:55:59 -07001073 .setPath(remotePathResolver.localPathToOutputPath(file))
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001074 .setDigest(digest)
1075 .setIsExecutable(file.isExecutable());
1076
1077 digestToFile.put(digest, file);
1078 }
1079
Benjamin Petersondd3ddb02018-05-03 09:20:08 -07001080 private void addDirectory(Path dir) throws ExecException, IOException {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001081 Tree.Builder tree = Tree.newBuilder();
1082 Directory root = computeDirectory(dir, tree);
1083 tree.setRoot(root);
1084
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -07001085 ByteString data = tree.build().toByteString();
1086 Digest digest = digestUtil.compute(data.toByteArray());
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001087
1088 if (result != null) {
1089 result
1090 .addOutputDirectoriesBuilder()
Chi Wangb863dbf2021-04-19 00:55:59 -07001091 .setPath(remotePathResolver.localPathToOutputPath(dir))
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001092 .setTreeDigest(digest);
1093 }
1094
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -07001095 digestToBlobs.put(digest, data);
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001096 }
1097
Benjamin Petersondd3ddb02018-05-03 09:20:08 -07001098 private Directory computeDirectory(Path path, Tree.Builder tree)
1099 throws ExecException, IOException {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001100 Directory.Builder b = Directory.newBuilder();
1101
olaolafbfb3cb2018-11-08 11:14:57 -08001102 List<Dirent> sortedDirent = new ArrayList<>(path.readdir(Symlinks.NOFOLLOW));
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001103 sortedDirent.sort(Comparator.comparing(Dirent::getName));
1104
1105 for (Dirent dirent : sortedDirent) {
1106 String name = dirent.getName();
1107 Path child = path.getRelative(name);
1108 if (dirent.getType() == Dirent.Type.DIRECTORY) {
1109 Directory dir = computeDirectory(child, tree);
1110 b.addDirectoriesBuilder().setName(name).setDigest(digestUtil.compute(dir));
1111 tree.addChildren(dir);
olaolafbfb3cb2018-11-08 11:14:57 -08001112 } else if (dirent.getType() == Dirent.Type.SYMLINK && allowSymlinks) {
1113 PathFragment target = child.readSymbolicLink();
1114 if (uploadSymlinks && !target.isAbsolute()) {
1115 // Whether it is dangling or not, we're passing it on.
1116 b.addSymlinksBuilder().setName(name).setTarget(target.toString());
1117 continue;
1118 }
1119 // Need to resolve the symbolic link now to know whether to upload a file or a directory.
1120 FileStatus statFollow = child.statIfFound(Symlinks.FOLLOW);
1121 if (statFollow == null) {
1122 throw new IOException(
1123 String.format(
1124 "Action output %s is a dangling symbolic link to %s ", child, target));
1125 }
1126 if (statFollow.isFile() && !statFollow.isSpecialFile()) {
1127 Digest digest = digestUtil.compute(child);
1128 b.addFilesBuilder()
1129 .setName(name)
1130 .setDigest(digest)
1131 .setIsExecutable(child.isExecutable());
1132 digestToFile.put(digest, child);
1133 } else if (statFollow.isDirectory()) {
1134 Directory dir = computeDirectory(child, tree);
1135 b.addDirectoriesBuilder().setName(name).setDigest(digestUtil.compute(dir));
1136 tree.addChildren(dir);
1137 } else {
1138 illegalOutput(child);
1139 }
1140 } else if (dirent.getType() == Dirent.Type.FILE) {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001141 Digest digest = digestUtil.compute(child);
1142 b.addFilesBuilder().setName(name).setDigest(digest).setIsExecutable(child.isExecutable());
1143 digestToFile.put(digest, child);
Benjamin Petersondd3ddb02018-05-03 09:20:08 -07001144 } else {
1145 illegalOutput(child);
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001146 }
1147 }
1148
1149 return b.build();
1150 }
Benjamin Petersondd3ddb02018-05-03 09:20:08 -07001151
Jakob Buchgraber584ae242019-03-29 08:58:41 -07001152 private void illegalOutput(Path what) throws ExecException {
Benjamin Petersondd3ddb02018-05-03 09:20:08 -07001153 String kind = what.isSymbolicLink() ? "symbolic link" : "special file";
mschaller07933882020-06-24 14:38:23 -07001154 String message =
Benjamin Petersondd3ddb02018-05-03 09:20:08 -07001155 String.format(
1156 "Output %s is a %s. Only regular files and directories may be "
1157 + "uploaded to a remote cache. "
1158 + "Change the file type or use --remote_allow_symlink_upload.",
Chi Wangb863dbf2021-04-19 00:55:59 -07001159 remotePathResolver.localPathToOutputPath(what), kind);
mschaller07933882020-06-24 14:38:23 -07001160 throw new UserExecException(createFailureDetail(message, Code.ILLEGAL_OUTPUT));
Benjamin Petersondd3ddb02018-05-03 09:20:08 -07001161 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001162 }
1163
1164 /** Release resources associated with the cache. The cache may not be used after calling this. */
1165 @Override
Jakob Buchgraber60566092019-11-11 06:28:22 -08001166 public void close() {
1167 cacheProtocol.close();
1168 }
buchgrff008f42018-06-02 14:13:43 -07001169
mschaller07933882020-06-24 14:38:23 -07001170 private static FailureDetail createFailureDetail(String message, Code detailedCode) {
1171 return FailureDetail.newBuilder()
1172 .setMessage(message)
1173 .setRemoteExecution(RemoteExecution.newBuilder().setCode(detailedCode))
1174 .build();
1175 }
1176
Chi Wang0f812eb2021-07-06 01:14:43 -07001177 /**
1178 * An {@link OutputStream} that reports all the write operations with {@link
1179 * DownloadProgressReporter}.
1180 */
1181 private static class ReportingOutputStream extends OutputStream {
1182
1183 private final OutputStream out;
1184 private final DownloadProgressReporter reporter;
1185
1186 ReportingOutputStream(OutputStream out, DownloadProgressReporter reporter) {
1187 this.out = out;
1188 this.reporter = reporter;
1189 }
1190
1191 @Override
1192 public void write(byte[] b) throws IOException {
1193 out.write(b);
1194 reporter.downloadedBytes(b.length);
1195 }
1196
1197 @Override
1198 public void write(byte[] b, int off, int len) throws IOException {
1199 out.write(b, off, len);
1200 reporter.downloadedBytes(len);
1201 }
1202
1203 @Override
1204 public void write(int b) throws IOException {
1205 out.write(b);
1206 reporter.downloadedBytes(1);
1207 }
1208
1209 @Override
1210 public void flush() throws IOException {
1211 out.flush();
1212 }
1213
1214 @Override
1215 public void close() throws IOException {
1216 out.close();
1217 }
1218 }
1219
Jakob Buchgraber584ae242019-03-29 08:58:41 -07001220 /** In-memory representation of action result metadata. */
1221 static class ActionResultMetadata {
1222
1223 static class SymlinkMetadata {
1224 private final Path path;
1225 private final PathFragment target;
1226
1227 private SymlinkMetadata(Path path, PathFragment target) {
1228 this.path = path;
1229 this.target = target;
1230 }
1231
1232 public Path path() {
1233 return path;
1234 }
1235
1236 public PathFragment target() {
1237 return target;
1238 }
1239 }
1240
1241 static class FileMetadata {
1242 private final Path path;
1243 private final Digest digest;
1244 private final boolean isExecutable;
1245
1246 private FileMetadata(Path path, Digest digest, boolean isExecutable) {
1247 this.path = path;
1248 this.digest = digest;
1249 this.isExecutable = isExecutable;
1250 }
1251
1252 public Path path() {
1253 return path;
1254 }
1255
1256 public Digest digest() {
1257 return digest;
1258 }
1259
1260 public boolean isExecutable() {
1261 return isExecutable;
1262 }
1263 }
1264
1265 static class DirectoryMetadata {
1266 private final ImmutableList<FileMetadata> files;
1267 private final ImmutableList<SymlinkMetadata> symlinks;
1268
1269 private DirectoryMetadata(
1270 ImmutableList<FileMetadata> files, ImmutableList<SymlinkMetadata> symlinks) {
1271 this.files = files;
1272 this.symlinks = symlinks;
1273 }
1274
1275 public ImmutableList<FileMetadata> files() {
1276 return files;
1277 }
1278
1279 public ImmutableList<SymlinkMetadata> symlinks() {
1280 return symlinks;
1281 }
1282 }
1283
1284 private final ImmutableMap<Path, FileMetadata> files;
1285 private final ImmutableMap<Path, SymlinkMetadata> symlinks;
1286 private final ImmutableMap<Path, DirectoryMetadata> directories;
1287
1288 private ActionResultMetadata(
1289 ImmutableMap<Path, FileMetadata> files,
1290 ImmutableMap<Path, SymlinkMetadata> symlinks,
1291 ImmutableMap<Path, DirectoryMetadata> directories) {
1292 this.files = files;
1293 this.symlinks = symlinks;
1294 this.directories = directories;
1295 }
1296
1297 @Nullable
1298 public FileMetadata file(Path path) {
1299 return files.get(path);
1300 }
1301
1302 @Nullable
1303 public DirectoryMetadata directory(Path path) {
1304 return directories.get(path);
1305 }
1306
1307 public Collection<FileMetadata> files() {
1308 return files.values();
1309 }
1310
1311 public ImmutableSet<Entry<Path, DirectoryMetadata>> directories() {
1312 return directories.entrySet();
1313 }
1314
1315 public Collection<SymlinkMetadata> symlinks() {
1316 return symlinks.values();
1317 }
1318 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001319}