blob: 59138b5a3c1c79ab8aef3f2442b966b4860d6681 [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;
George Gensureaeee3e02020-04-15 04:43:45 -070018import static com.google.devtools.build.lib.remote.util.Utils.getFromFuture;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070019
olaolaf0aa55d2018-08-16 08:51:06 -070020import build.bazel.remote.execution.v2.Action;
21import build.bazel.remote.execution.v2.ActionResult;
22import build.bazel.remote.execution.v2.Command;
23import build.bazel.remote.execution.v2.Digest;
24import build.bazel.remote.execution.v2.Directory;
25import build.bazel.remote.execution.v2.DirectoryNode;
26import build.bazel.remote.execution.v2.FileNode;
27import build.bazel.remote.execution.v2.OutputDirectory;
28import build.bazel.remote.execution.v2.OutputFile;
olaolafbfb3cb2018-11-08 11:14:57 -080029import build.bazel.remote.execution.v2.OutputSymlink;
30import build.bazel.remote.execution.v2.SymlinkNode;
olaolaf0aa55d2018-08-16 08:51:06 -070031import build.bazel.remote.execution.v2.Tree;
buchgrff008f42018-06-02 14:13:43 -070032import com.google.common.base.Preconditions;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070033import com.google.common.collect.ImmutableList;
34import com.google.common.collect.ImmutableMap;
35import com.google.common.collect.ImmutableSet;
olaolafbfb3cb2018-11-08 11:14:57 -080036import com.google.common.collect.Iterables;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070037import com.google.common.collect.Maps;
Chi Wangc7c7f5f2020-11-09 22:29:35 -080038import com.google.common.flogger.GoogleLogger;
buchgrff008f42018-06-02 14:13:43 -070039import com.google.common.util.concurrent.FutureCallback;
40import com.google.common.util.concurrent.Futures;
41import com.google.common.util.concurrent.ListenableFuture;
buchgrff008f42018-06-02 14:13:43 -070042import com.google.common.util.concurrent.SettableFuture;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070043import com.google.devtools.build.lib.actions.ActionInput;
44import com.google.devtools.build.lib.actions.Artifact;
Googler04c546f2020-05-12 18:22:18 -070045import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
46import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080047import com.google.devtools.build.lib.actions.EnvironmentalExecException;
48import com.google.devtools.build.lib.actions.ExecException;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -070049import com.google.devtools.build.lib.actions.UserExecException;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070050import com.google.devtools.build.lib.actions.cache.MetadataInjector;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080051import com.google.devtools.build.lib.concurrent.ThreadSafety;
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -070052import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionContext;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070053import com.google.devtools.build.lib.profiler.Profiler;
54import com.google.devtools.build.lib.profiler.SilentCloseable;
Jakob Buchgraber60566092019-11-11 06:28:22 -080055import com.google.devtools.build.lib.remote.RemoteCache.ActionResultMetadata.DirectoryMetadata;
56import com.google.devtools.build.lib.remote.RemoteCache.ActionResultMetadata.FileMetadata;
57import com.google.devtools.build.lib.remote.RemoteCache.ActionResultMetadata.SymlinkMetadata;
Googlerbc54c642021-01-26 01:24:39 -080058import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext;
Chi Wangb6e3ba82021-01-14 21:57:28 -080059import com.google.devtools.build.lib.remote.common.RemoteActionFileArtifactValue;
Jakob Buchgraber60566092019-11-11 06:28:22 -080060import com.google.devtools.build.lib.remote.common.RemoteCacheClient;
61import com.google.devtools.build.lib.remote.common.RemoteCacheClient.ActionKey;
Jakob Buchgraber75b7ed42019-03-27 10:27:13 -070062import com.google.devtools.build.lib.remote.options.RemoteOptions;
Googler922d1e62018-03-05 14:49:00 -080063import com.google.devtools.build.lib.remote.util.DigestUtil;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070064import com.google.devtools.build.lib.remote.util.Utils.InMemoryOutput;
mschaller1eabf522020-06-10 22:03:13 -070065import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
66import com.google.devtools.build.lib.server.FailureDetails.RemoteExecution;
67import com.google.devtools.build.lib.server.FailureDetails.RemoteExecution.Code;
jhorvitzed7ec3b2020-07-24 15:08:03 -070068import com.google.devtools.build.lib.skyframe.TreeArtifactValue;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080069import com.google.devtools.build.lib.util.io.FileOutErr;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070070import com.google.devtools.build.lib.util.io.OutErr;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080071import com.google.devtools.build.lib.vfs.Dirent;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -070072import com.google.devtools.build.lib.vfs.FileStatus;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080073import com.google.devtools.build.lib.vfs.FileSystemUtils;
74import com.google.devtools.build.lib.vfs.Path;
olaolafbfb3cb2018-11-08 11:14:57 -080075import com.google.devtools.build.lib.vfs.PathFragment;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -070076import com.google.devtools.build.lib.vfs.Symlinks;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070077import com.google.protobuf.ByteString;
78import com.google.protobuf.InvalidProtocolBufferException;
buchgrff008f42018-06-02 14:13:43 -070079import java.io.ByteArrayOutputStream;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080080import java.io.IOException;
81import java.io.OutputStream;
82import java.util.ArrayList;
83import java.util.Collection;
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -070084import java.util.Collections;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080085import java.util.Comparator;
86import java.util.HashMap;
87import java.util.List;
88import java.util.Map;
Jakob Buchgraber584ae242019-03-29 08:58:41 -070089import java.util.Map.Entry;
90import java.util.stream.Collectors;
91import java.util.stream.Stream;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -080092import javax.annotation.Nullable;
93
94/** A cache for storing artifacts (input and output) as well as the output of running an action. */
95@ThreadSafety.ThreadSafe
Jakob Buchgraber60566092019-11-11 06:28:22 -080096public class RemoteCache implements AutoCloseable {
Chi Wangc7c7f5f2020-11-09 22:29:35 -080097 private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
buchgrff008f42018-06-02 14:13:43 -070098
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -070099 /** See {@link SpawnExecutionContext#lockOutputFiles()}. */
100 @FunctionalInterface
101 interface OutputFilesLocker {
michajlo6bc145b2020-10-16 14:41:35 -0700102 void lock() throws InterruptedException;
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700103 }
104
jhorvitzed7ec3b2020-07-24 15:08:03 -0700105 private static final ListenableFuture<Void> COMPLETED_SUCCESS = immediateFuture(null);
106 private static final ListenableFuture<byte[]> EMPTY_BYTES = immediateFuture(new byte[0]);
buchgrff008f42018-06-02 14:13:43 -0700107
Jakob Buchgraber60566092019-11-11 06:28:22 -0800108 protected final RemoteCacheClient cacheProtocol;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700109 protected final RemoteOptions options;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800110 protected final DigestUtil digestUtil;
111
Jakob Buchgraber60566092019-11-11 06:28:22 -0800112 public RemoteCache(
113 RemoteCacheClient cacheProtocol, RemoteOptions options, DigestUtil digestUtil) {
114 this.cacheProtocol = cacheProtocol;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700115 this.options = options;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800116 this.digestUtil = digestUtil;
117 }
118
Googlerbc54c642021-01-26 01:24:39 -0800119 public ActionResult downloadActionResult(
120 RemoteActionExecutionContext context, ActionKey actionKey, boolean inlineOutErr)
Jakob Buchgraber60566092019-11-11 06:28:22 -0800121 throws IOException, InterruptedException {
Googlerbc54c642021-01-26 01:24:39 -0800122 return getFromFuture(cacheProtocol.downloadActionResult(context, actionKey, inlineOutErr));
Jakob Buchgraber60566092019-11-11 06:28:22 -0800123 }
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700124
buchgr12ebb842019-08-21 02:03:24 -0700125 /**
126 * Upload the result of a locally executed action to the remote cache.
127 *
128 * @throws IOException if there was an error uploading to the remote cache
129 * @throws ExecException if uploading any of the action outputs is not supported
130 */
131 public ActionResult upload(
132 ActionKey actionKey,
133 Action action,
134 Command command,
135 Path execRoot,
136 Collection<Path> outputs,
137 FileOutErr outErr,
138 int exitCode)
139 throws ExecException, IOException, InterruptedException {
140 ActionResult.Builder resultBuilder = ActionResult.newBuilder();
141 uploadOutputs(execRoot, actionKey, action, command, outputs, outErr, resultBuilder);
142 resultBuilder.setExitCode(exitCode);
143 ActionResult result = resultBuilder.build();
144 if (exitCode == 0 && !action.getDoNotCache()) {
Jakob Buchgraber60566092019-11-11 06:28:22 -0800145 cacheProtocol.uploadActionResult(actionKey, result);
buchgr12ebb842019-08-21 02:03:24 -0700146 }
147 return result;
148 }
149
150 public ActionResult upload(
151 ActionKey actionKey,
152 Action action,
153 Command command,
154 Path execRoot,
155 Collection<Path> outputs,
156 FileOutErr outErr)
157 throws ExecException, IOException, InterruptedException {
158 return upload(actionKey, action, command, execRoot, outputs, outErr, /* exitCode= */ 0);
159 }
160
161 private void uploadOutputs(
162 Path execRoot,
163 ActionKey actionKey,
164 Action action,
165 Command command,
166 Collection<Path> files,
167 FileOutErr outErr,
168 ActionResult.Builder result)
169 throws ExecException, IOException, InterruptedException {
170 UploadManifest manifest =
171 new UploadManifest(
172 digestUtil,
173 result,
174 execRoot,
175 options.incompatibleRemoteSymlinks,
176 options.allowSymlinkUpload);
177 manifest.addFiles(files);
178 manifest.setStdoutStderr(outErr);
179 manifest.addAction(actionKey, action, command);
180
181 Map<Digest, Path> digestToFile = manifest.getDigestToFile();
182 Map<Digest, ByteString> digestToBlobs = manifest.getDigestToBlobs();
183 Collection<Digest> digests = new ArrayList<>();
184 digests.addAll(digestToFile.keySet());
185 digests.addAll(digestToBlobs.keySet());
186
George Gensureaeee3e02020-04-15 04:43:45 -0700187 ImmutableSet<Digest> digestsToUpload = getFromFuture(cacheProtocol.findMissingDigests(digests));
buchgr12ebb842019-08-21 02:03:24 -0700188 ImmutableList.Builder<ListenableFuture<Void>> uploads = ImmutableList.builder();
189 for (Digest digest : digestsToUpload) {
190 Path file = digestToFile.get(digest);
191 if (file != null) {
Jakob Buchgraber60566092019-11-11 06:28:22 -0800192 uploads.add(cacheProtocol.uploadFile(digest, file));
buchgr12ebb842019-08-21 02:03:24 -0700193 } else {
194 ByteString blob = digestToBlobs.get(digest);
195 if (blob == null) {
196 String message = "FindMissingBlobs call returned an unknown digest: " + digest;
197 throw new IOException(message);
198 }
Jakob Buchgraber60566092019-11-11 06:28:22 -0800199 uploads.add(cacheProtocol.uploadBlob(digest, blob));
buchgr12ebb842019-08-21 02:03:24 -0700200 }
201 }
202
George Gensureaeee3e02020-04-15 04:43:45 -0700203 waitForBulkTransfer(uploads.build(), /* cancelRemainingOnInterrupt=*/ false);
buchgr12ebb842019-08-21 02:03:24 -0700204
205 if (manifest.getStderrDigest() != null) {
206 result.setStderrDigest(manifest.getStderrDigest());
207 }
208 if (manifest.getStdoutDigest() != null) {
209 result.setStdoutDigest(manifest.getStdoutDigest());
210 }
211 }
212
Ulf Adams4f008c52020-09-17 01:17:59 -0700213 public static void waitForBulkTransfer(
214 Iterable<? extends ListenableFuture<?>> transfers, boolean cancelRemainingOnInterrupt)
George Gensureaeee3e02020-04-15 04:43:45 -0700215 throws BulkTransferException, InterruptedException {
216 BulkTransferException bulkTransferException = null;
217 InterruptedException interruptedException = null;
218 boolean interrupted = Thread.currentThread().isInterrupted();
Ulf Adams4f008c52020-09-17 01:17:59 -0700219 for (ListenableFuture<?> transfer : transfers) {
George Gensureaeee3e02020-04-15 04:43:45 -0700220 try {
221 if (interruptedException == null) {
George Gensure24f97e12020-04-17 05:42:46 -0700222 // Wait for all transfers to finish.
Chi Wang62e169a2020-11-12 00:26:32 -0800223 getFromFuture(transfer, cancelRemainingOnInterrupt);
George Gensureaeee3e02020-04-15 04:43:45 -0700224 } else {
225 transfer.cancel(true);
226 }
227 } catch (IOException e) {
228 if (bulkTransferException == null) {
229 bulkTransferException = new BulkTransferException();
230 }
231 bulkTransferException.add(e);
232 } catch (InterruptedException e) {
233 interrupted = Thread.interrupted() || interrupted;
234 interruptedException = e;
235 if (!cancelRemainingOnInterrupt) {
236 // leave the rest of the transfers alone
237 break;
238 }
buchgr12ebb842019-08-21 02:03:24 -0700239 }
George Gensureaeee3e02020-04-15 04:43:45 -0700240 }
241 if (interrupted) {
242 Thread.currentThread().interrupt();
243 }
244 if (interruptedException != null) {
245 if (bulkTransferException != null) {
246 interruptedException.addSuppressed(bulkTransferException);
buchgr12ebb842019-08-21 02:03:24 -0700247 }
George Gensureaeee3e02020-04-15 04:43:45 -0700248 throw interruptedException;
249 }
250 if (bulkTransferException != null) {
251 throw bulkTransferException;
buchgr12ebb842019-08-21 02:03:24 -0700252 }
253 }
254
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700255 /**
buchgrff008f42018-06-02 14:13:43 -0700256 * Downloads a blob with content hash {@code digest} and stores its content in memory.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800257 *
buchgrff008f42018-06-02 14:13:43 -0700258 * @return a future that completes after the download completes (succeeds / fails). If successful,
259 * the content is stored in the future's {@code byte[]}.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800260 */
buchgrff008f42018-06-02 14:13:43 -0700261 public ListenableFuture<byte[]> downloadBlob(Digest digest) {
262 if (digest.getSizeBytes() == 0) {
263 return EMPTY_BYTES;
264 }
265 ByteArrayOutputStream bOut = new ByteArrayOutputStream((int) digest.getSizeBytes());
266 SettableFuture<byte[]> outerF = SettableFuture.create();
267 Futures.addCallback(
Jakob Buchgraber60566092019-11-11 06:28:22 -0800268 cacheProtocol.downloadBlob(digest, bOut),
buchgrff008f42018-06-02 14:13:43 -0700269 new FutureCallback<Void>() {
270 @Override
271 public void onSuccess(Void aVoid) {
Chi Wangc7c7f5f2020-11-09 22:29:35 -0800272 try {
273 outerF.set(bOut.toByteArray());
274 } catch (RuntimeException e) {
275 logger.atWarning().withCause(e).log("Unexpected exception");
276 outerF.setException(e);
277 }
buchgrff008f42018-06-02 14:13:43 -0700278 }
279
280 @Override
281 public void onFailure(Throwable t) {
282 outerF.setException(t);
283 }
284 },
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700285 directExecutor());
buchgrff008f42018-06-02 14:13:43 -0700286 return outerF;
287 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800288
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700289 private static Path toTmpDownloadPath(Path actualPath) {
290 return actualPath.getParentDirectory().getRelative(actualPath.getBaseName() + ".tmp");
291 }
292
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800293 /**
294 * Download the output files and directory trees of a remotely executed action to the local
295 * machine, as well stdin / stdout to the given files.
296 *
297 * <p>In case of failure, this method deletes any output files it might have already created.
298 *
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700299 * @param outputFilesLocker ensures that we are the only ones writing to the output files when
300 * using the dynamic spawn strategy.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800301 * @throws IOException in case of a cache miss or if the remote cache is unavailable.
302 * @throws ExecException in case clean up after a failed download failed.
303 */
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700304 public void download(
305 ActionResult result,
306 Path execRoot,
307 FileOutErr origOutErr,
308 OutputFilesLocker outputFilesLocker)
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800309 throws ExecException, IOException, InterruptedException {
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700310 ActionResultMetadata metadata = parseActionResultMetadata(result, execRoot);
buchgrff008f42018-06-02 14:13:43 -0700311
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700312 List<ListenableFuture<FileMetadata>> downloads =
313 Stream.concat(
314 metadata.files().stream(),
315 metadata.directories().stream()
316 .flatMap((entry) -> entry.getValue().files().stream()))
317 .map(
318 (file) -> {
319 try {
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700320 ListenableFuture<Void> download =
321 downloadFile(toTmpDownloadPath(file.path()), file.digest());
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700322 return Futures.transform(download, (d) -> file, directExecutor());
323 } catch (IOException e) {
324 return Futures.<FileMetadata>immediateFailedFuture(e);
325 }
326 })
327 .collect(Collectors.toList());
buchgrff008f42018-06-02 14:13:43 -0700328
buchgrbca191282018-07-25 05:40:42 -0700329 // Subsequently we need to wait for *every* download to finish, even if we already know that
330 // one failed. That's so that when exiting this method we can be sure that all downloads have
331 // finished and don't race with the cleanup routine.
buchgrbca191282018-07-25 05:40:42 -0700332
pcloudy130f86d2019-04-29 05:55:23 -0700333 FileOutErr tmpOutErr = null;
George Gensureaeee3e02020-04-15 04:43:45 -0700334 if (origOutErr != null) {
335 tmpOutErr = origOutErr.childOutErr();
336 }
337 downloads.addAll(downloadOutErr(result, tmpOutErr));
338
buchgrbca191282018-07-25 05:40:42 -0700339 try {
George Gensureaeee3e02020-04-15 04:43:45 -0700340 waitForBulkTransfer(downloads, /* cancelRemainingOnInterrupt=*/ true);
341 } catch (Exception e) {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800342 try {
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700343 // Delete any (partially) downloaded output files.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800344 for (OutputFile file : result.getOutputFilesList()) {
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700345 toTmpDownloadPath(execRoot.getRelative(file.getPath())).delete();
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800346 }
347 for (OutputDirectory directory : result.getOutputDirectoriesList()) {
Keith Smiley4392ba42018-12-10 02:39:37 -0800348 // Only delete the directories below the output directories because the output
349 // directories will not be re-created
jmmv5cc1f652019-03-20 09:34:08 -0700350 execRoot.getRelative(directory.getPath()).deleteTreesBelow();
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800351 }
pcloudy130f86d2019-04-29 05:55:23 -0700352 if (tmpOutErr != null) {
353 tmpOutErr.clearOut();
354 tmpOutErr.clearErr();
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800355 }
George Gensureaeee3e02020-04-15 04:43:45 -0700356 } catch (IOException ioEx) {
357 ioEx.addSuppressed(e);
Jakob Buchgraber4dc78a02019-09-26 07:53:11 -0700358
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800359 // If deleting of output files failed, we abort the build with a decent error message as
360 // any subsequent local execution failure would likely be incomprehensible.
George Gensureaeee3e02020-04-15 04:43:45 -0700361 ExecException execEx =
362 new EnvironmentalExecException(
mschaller1eabf522020-06-10 22:03:13 -0700363 ioEx,
mschaller07933882020-06-24 14:38:23 -0700364 createFailureDetail(
365 "Failed to delete output files after incomplete download",
366 Code.INCOMPLETE_OUTPUT_DOWNLOAD_CLEANUP_FAILURE));
George Gensureaeee3e02020-04-15 04:43:45 -0700367 execEx.addSuppressed(e);
368 throw execEx;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800369 }
George Gensureaeee3e02020-04-15 04:43:45 -0700370 throw e;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800371 }
olaolafbfb3cb2018-11-08 11:14:57 -0800372
pcloudy130f86d2019-04-29 05:55:23 -0700373 if (tmpOutErr != null) {
374 FileOutErr.dump(tmpOutErr, origOutErr);
375 tmpOutErr.clearOut();
376 tmpOutErr.clearErr();
377 }
378
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700379 // Ensure that we are the only ones writing to the output files when using the dynamic spawn
380 // strategy.
381 outputFilesLocker.lock();
382
383 moveOutputsToFinalLocation(downloads);
384
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700385 List<SymlinkMetadata> symlinksInDirectories = new ArrayList<>();
386 for (Entry<Path, DirectoryMetadata> entry : metadata.directories()) {
387 entry.getKey().createDirectoryAndParents();
388 symlinksInDirectories.addAll(entry.getValue().symlinks());
389 }
390
391 Iterable<SymlinkMetadata> symlinks =
392 Iterables.concat(metadata.symlinks(), symlinksInDirectories);
393
394 // Create the symbolic links after all downloads are finished, because dangling symlinks
395 // might not be supported on all platforms
396 createSymlinks(symlinks);
olaolafbfb3cb2018-11-08 11:14:57 -0800397 }
398
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700399 /**
400 * Copies moves the downloaded outputs from their download location to their declared location.
401 */
402 private void moveOutputsToFinalLocation(List<ListenableFuture<FileMetadata>> downloads)
403 throws IOException, InterruptedException {
404 List<FileMetadata> finishedDownloads = new ArrayList<>(downloads.size());
405 for (ListenableFuture<FileMetadata> finishedDownload : downloads) {
406 FileMetadata outputFile = getFromFuture(finishedDownload);
407 if (outputFile != null) {
408 finishedDownloads.add(outputFile);
409 }
410 }
411 /*
412 * Sort the list lexicographically based on its temporary download path in order to avoid
413 * filename clashes when moving the files:
414 *
415 * Consider an action that produces two outputs foo and foo.tmp. These outputs would initially
416 * be downloaded to foo.tmp and foo.tmp.tmp. When renaming them to foo and foo.tmp we need to
417 * ensure that rename(foo.tmp, foo) happens before rename(foo.tmp.tmp, foo.tmp). We ensure this
418 * by doing the renames in lexicographical order of the download names.
419 */
420 Collections.sort(finishedDownloads, Comparator.comparing(f -> toTmpDownloadPath(f.path())));
421
422 // Move the output files from their temporary name to the actual output file name.
423 for (FileMetadata outputFile : finishedDownloads) {
424 FileSystemUtils.moveFile(toTmpDownloadPath(outputFile.path()), outputFile.path());
425 outputFile.path().setExecutable(outputFile.isExecutable());
426 }
427 }
428
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700429 private void createSymlinks(Iterable<SymlinkMetadata> symlinks) throws IOException {
430 for (SymlinkMetadata symlink : symlinks) {
431 if (symlink.target().isAbsolute()) {
432 // We do not support absolute symlinks as outputs.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800433 throw new IOException(
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700434 String.format(
435 "Action output %s is a symbolic link to an absolute path %s. "
436 + "Symlinks to absolute paths in action outputs are not supported.",
437 symlink.path(), symlink.target()));
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800438 }
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700439 Preconditions.checkNotNull(
440 symlink.path().getParentDirectory(),
441 "Failed creating directory and parents for %s",
442 symlink.path())
443 .createDirectoryAndParents();
444 symlink.path().createSymbolicLink(symlink.target());
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800445 }
446 }
447
jhorvitzed7ec3b2020-07-24 15:08:03 -0700448 /** Downloads a file (that is not a directory). The content is fetched from the digest. */
olaolaf0aa55d2018-08-16 08:51:06 -0700449 public ListenableFuture<Void> downloadFile(Path path, Digest digest) throws IOException {
buchgrff008f42018-06-02 14:13:43 -0700450 Preconditions.checkNotNull(path.getParentDirectory()).createDirectoryAndParents();
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800451 if (digest.getSizeBytes() == 0) {
452 // Handle empty file locally.
453 FileSystemUtils.writeContent(path, new byte[0]);
buchgrff008f42018-06-02 14:13:43 -0700454 return COMPLETED_SUCCESS;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800455 }
buchgrff008f42018-06-02 14:13:43 -0700456
Ed Schoutencb08ffc2020-09-09 23:32:01 -0700457 if (!options.remoteDownloadSymlinkTemplate.isEmpty()) {
458 // Don't actually download files from the CAS. Instead, create a
459 // symbolic link that points to a location where CAS objects may
460 // be found. This could, for example, be a FUSE file system.
461 path.createSymbolicLink(
462 path.getRelative(
463 options
464 .remoteDownloadSymlinkTemplate
465 .replace("{hash}", digest.getHash())
466 .replace("{size_bytes}", String.valueOf(digest.getSizeBytes()))));
467 return COMPLETED_SUCCESS;
468 }
469
buchgrff008f42018-06-02 14:13:43 -0700470 OutputStream out = new LazyFileOutputStream(path);
471 SettableFuture<Void> outerF = SettableFuture.create();
Jakob Buchgraber60566092019-11-11 06:28:22 -0800472 ListenableFuture<Void> f = cacheProtocol.downloadBlob(digest, out);
buchgrff008f42018-06-02 14:13:43 -0700473 Futures.addCallback(
474 f,
475 new FutureCallback<Void>() {
476 @Override
477 public void onSuccess(Void result) {
478 try {
479 out.close();
480 outerF.set(null);
481 } catch (IOException e) {
482 outerF.setException(e);
Chi Wangc7c7f5f2020-11-09 22:29:35 -0800483 } catch (RuntimeException e) {
484 logger.atWarning().withCause(e).log("Unexpected exception");
485 outerF.setException(e);
buchgrff008f42018-06-02 14:13:43 -0700486 }
487 }
488
489 @Override
490 public void onFailure(Throwable t) {
buchgrff008f42018-06-02 14:13:43 -0700491 try {
492 out.close();
493 } catch (IOException e) {
Jakob Buchgraber4dc78a02019-09-26 07:53:11 -0700494 if (t != e) {
495 t.addSuppressed(e);
496 }
Chi Wangc7c7f5f2020-11-09 22:29:35 -0800497 } catch (RuntimeException e) {
498 logger.atWarning().withCause(e).log("Unexpected exception");
499 t.addSuppressed(e);
pcloudya3a59752019-01-07 03:48:37 -0800500 } finally {
501 outerF.setException(t);
buchgrff008f42018-06-02 14:13:43 -0700502 }
503 }
504 },
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700505 directExecutor());
buchgrff008f42018-06-02 14:13:43 -0700506 return outerF;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800507 }
508
George Gensureaeee3e02020-04-15 04:43:45 -0700509 private List<ListenableFuture<FileMetadata>> downloadOutErr(ActionResult result, OutErr outErr) {
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700510 List<ListenableFuture<FileMetadata>> downloads = new ArrayList<>();
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800511 if (!result.getStdoutRaw().isEmpty()) {
George Gensureaeee3e02020-04-15 04:43:45 -0700512 try {
513 result.getStdoutRaw().writeTo(outErr.getOutputStream());
514 outErr.getOutputStream().flush();
515 } catch (IOException e) {
516 downloads.add(Futures.immediateFailedFuture(e));
517 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800518 } else if (result.hasStdoutDigest()) {
buchgrff008f42018-06-02 14:13:43 -0700519 downloads.add(
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700520 Futures.transform(
Jakob Buchgraber60566092019-11-11 06:28:22 -0800521 cacheProtocol.downloadBlob(result.getStdoutDigest(), outErr.getOutputStream()),
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700522 (d) -> null,
523 directExecutor()));
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800524 }
525 if (!result.getStderrRaw().isEmpty()) {
George Gensureaeee3e02020-04-15 04:43:45 -0700526 try {
527 result.getStderrRaw().writeTo(outErr.getErrorStream());
528 outErr.getErrorStream().flush();
529 } catch (IOException e) {
530 downloads.add(Futures.immediateFailedFuture(e));
531 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800532 } else if (result.hasStderrDigest()) {
buchgrff008f42018-06-02 14:13:43 -0700533 downloads.add(
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700534 Futures.transform(
Jakob Buchgraber60566092019-11-11 06:28:22 -0800535 cacheProtocol.downloadBlob(result.getStderrDigest(), outErr.getErrorStream()),
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700536 (d) -> null,
537 directExecutor()));
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800538 }
buchgrff008f42018-06-02 14:13:43 -0700539 return downloads;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800540 }
541
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700542 /**
543 * Avoids downloading the majority of action outputs but injects their metadata using {@link
544 * MetadataInjector} instead.
545 *
546 * <p>This method only downloads output directory metadata, stdout and stderr as well as the
547 * contents of {@code inMemoryOutputPath} if specified.
548 *
549 * @param result the action result metadata of a successfully executed action (exit code = 0).
550 * @param outputs the action's declared output files
551 * @param inMemoryOutputPath the path of an output file whose contents should be returned in
552 * memory by this method.
553 * @param outErr stdout and stderr of this action
554 * @param execRoot the execution root
555 * @param metadataInjector the action's metadata injector that allows this method to inject
556 * metadata about an action output instead of downloading the output
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700557 * @param outputFilesLocker ensures that we are the only ones writing to the output files when
558 * using the dynamic spawn strategy.
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700559 * @throws IOException in case of failure
560 * @throws InterruptedException in case of receiving an interrupt
561 */
562 @Nullable
563 public InMemoryOutput downloadMinimal(
George Gensure3ef8fb92020-05-06 09:49:48 -0700564 String actionId,
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700565 ActionResult result,
566 Collection<? extends ActionInput> outputs,
567 @Nullable PathFragment inMemoryOutputPath,
568 OutErr outErr,
569 Path execRoot,
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700570 MetadataInjector metadataInjector,
571 OutputFilesLocker outputFilesLocker)
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700572 throws IOException, InterruptedException {
573 Preconditions.checkState(
574 result.getExitCode() == 0,
575 "injecting remote metadata is only supported for successful actions (exit code 0).");
576
577 ActionResultMetadata metadata;
578 try (SilentCloseable c = Profiler.instance().profile("Remote.parseActionResultMetadata")) {
579 metadata = parseActionResultMetadata(result, execRoot);
580 }
581
582 if (!metadata.symlinks().isEmpty()) {
583 throw new IOException(
584 "Symlinks in action outputs are not yet supported by "
buchgrd480c5f2019-04-03 00:53:34 -0700585 + "--experimental_remote_download_outputs=minimal");
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700586 }
587
Jakob Buchgraberd75b6cf2019-06-19 08:12:49 -0700588 // Ensure that when using dynamic spawn strategy that we are the only ones writing to the
589 // output files.
590 outputFilesLocker.lock();
591
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700592 ActionInput inMemoryOutput = null;
593 Digest inMemoryOutputDigest = null;
594 for (ActionInput output : outputs) {
595 if (inMemoryOutputPath != null && output.getExecPath().equals(inMemoryOutputPath)) {
596 Path p = execRoot.getRelative(output.getExecPath());
Jakob Buchgrabere05dca32020-04-28 04:29:27 -0700597 FileMetadata m = metadata.file(p);
598 if (m == null) {
599 // A declared output wasn't created. Ignore it here. SkyFrame will fail if not all
600 // outputs were created.
601 continue;
602 }
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700603 inMemoryOutputDigest = m.digest();
604 inMemoryOutput = output;
605 }
606 if (output instanceof Artifact) {
George Gensure3ef8fb92020-05-06 09:49:48 -0700607 injectRemoteArtifact((Artifact) output, metadata, execRoot, metadataInjector, actionId);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700608 }
609 }
610
611 try (SilentCloseable c = Profiler.instance().profile("Remote.download")) {
612 ListenableFuture<byte[]> inMemoryOutputDownload = null;
613 if (inMemoryOutput != null) {
614 inMemoryOutputDownload = downloadBlob(inMemoryOutputDigest);
615 }
George Gensure7cf5a5e2020-04-28 08:48:19 -0700616 waitForBulkTransfer(downloadOutErr(result, outErr), /* cancelRemainingOnInterrupt=*/ true);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700617 if (inMemoryOutputDownload != null) {
George Gensure7cf5a5e2020-04-28 08:48:19 -0700618 waitForBulkTransfer(
619 ImmutableList.of(inMemoryOutputDownload), /* cancelRemainingOnInterrupt=*/ true);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700620 byte[] data = getFromFuture(inMemoryOutputDownload);
621 return new InMemoryOutput(inMemoryOutput, ByteString.copyFrom(data));
622 }
623 }
624 return null;
625 }
626
627 private void injectRemoteArtifact(
628 Artifact output,
629 ActionResultMetadata metadata,
630 Path execRoot,
George Gensure3ef8fb92020-05-06 09:49:48 -0700631 MetadataInjector metadataInjector,
632 String actionId)
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700633 throws IOException {
634 if (output.isTreeArtifact()) {
635 DirectoryMetadata directory =
636 metadata.directory(execRoot.getRelative(output.getExecPathString()));
637 if (directory == null) {
638 // A declared output wasn't created. It might have been an optional output and if not
639 // SkyFrame will make sure to fail.
640 return;
641 }
642 if (!directory.symlinks().isEmpty()) {
643 throw new IOException(
644 "Symlinks in action outputs are not yet supported by "
buchgrd480c5f2019-04-03 00:53:34 -0700645 + "--experimental_remote_download_outputs=minimal");
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700646 }
Googler04c546f2020-05-12 18:22:18 -0700647 SpecialArtifact parent = (SpecialArtifact) output;
jhorvitzed7ec3b2020-07-24 15:08:03 -0700648 TreeArtifactValue.Builder tree = TreeArtifactValue.newBuilder(parent);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700649 for (FileMetadata file : directory.files()) {
Googler04c546f2020-05-12 18:22:18 -0700650 TreeFileArtifact child =
Googler1d8d1382020-05-18 12:10:49 -0700651 TreeFileArtifact.createTreeOutput(parent, file.path().relativeTo(parent.getPath()));
Chi Wangb6e3ba82021-01-14 21:57:28 -0800652 RemoteActionFileArtifactValue value =
653 new RemoteActionFileArtifactValue(
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700654 DigestUtil.toBinaryDigest(file.digest()),
655 file.digest().getSizeBytes(),
Googler04c546f2020-05-12 18:22:18 -0700656 /*locationIndex=*/ 1,
Chi Wangb6e3ba82021-01-14 21:57:28 -0800657 actionId,
658 file.isExecutable());
jhorvitzed7ec3b2020-07-24 15:08:03 -0700659 tree.putChild(child, value);
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700660 }
jhorvitzed7ec3b2020-07-24 15:08:03 -0700661 metadataInjector.injectTree(parent, tree.build());
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700662 } else {
663 FileMetadata outputMetadata = metadata.file(execRoot.getRelative(output.getExecPathString()));
664 if (outputMetadata == null) {
665 // A declared output wasn't created. It might have been an optional output and if not
666 // SkyFrame will make sure to fail.
667 return;
668 }
Googleref554462020-06-04 17:40:55 -0700669 metadataInjector.injectFile(
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700670 output,
Chi Wangb6e3ba82021-01-14 21:57:28 -0800671 new RemoteActionFileArtifactValue(
Googlerb67aaba2020-05-13 09:49:37 -0700672 DigestUtil.toBinaryDigest(outputMetadata.digest()),
673 outputMetadata.digest().getSizeBytes(),
674 /*locationIndex=*/ 1,
Chi Wangb6e3ba82021-01-14 21:57:28 -0800675 actionId,
676 outputMetadata.isExecutable()));
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700677 }
678 }
679
680 private DirectoryMetadata parseDirectory(
681 Path parent, Directory dir, Map<Digest, Directory> childDirectoriesMap) {
682 ImmutableList.Builder<FileMetadata> filesBuilder = ImmutableList.builder();
683 for (FileNode file : dir.getFilesList()) {
684 filesBuilder.add(
685 new FileMetadata(
686 parent.getRelative(file.getName()), file.getDigest(), file.getIsExecutable()));
687 }
688
689 ImmutableList.Builder<SymlinkMetadata> symlinksBuilder = ImmutableList.builder();
690 for (SymlinkNode symlink : dir.getSymlinksList()) {
691 symlinksBuilder.add(
692 new SymlinkMetadata(
693 parent.getRelative(symlink.getName()), PathFragment.create(symlink.getTarget())));
694 }
695
696 for (DirectoryNode directoryNode : dir.getDirectoriesList()) {
697 Path childPath = parent.getRelative(directoryNode.getName());
698 Directory childDir =
699 Preconditions.checkNotNull(childDirectoriesMap.get(directoryNode.getDigest()));
700 DirectoryMetadata childMetadata = parseDirectory(childPath, childDir, childDirectoriesMap);
701 filesBuilder.addAll(childMetadata.files());
702 symlinksBuilder.addAll(childMetadata.symlinks());
703 }
704
705 return new DirectoryMetadata(filesBuilder.build(), symlinksBuilder.build());
706 }
707
708 private ActionResultMetadata parseActionResultMetadata(ActionResult actionResult, Path execRoot)
709 throws IOException, InterruptedException {
710 Preconditions.checkNotNull(actionResult, "actionResult");
711 Map<Path, ListenableFuture<Tree>> dirMetadataDownloads =
712 Maps.newHashMapWithExpectedSize(actionResult.getOutputDirectoriesCount());
713 for (OutputDirectory dir : actionResult.getOutputDirectoriesList()) {
714 dirMetadataDownloads.put(
715 execRoot.getRelative(dir.getPath()),
716 Futures.transform(
717 downloadBlob(dir.getTreeDigest()),
718 (treeBytes) -> {
719 try {
720 return Tree.parseFrom(treeBytes);
721 } catch (InvalidProtocolBufferException e) {
722 throw new RuntimeException(e);
723 }
724 },
725 directExecutor()));
726 }
727
George Gensure24f97e12020-04-17 05:42:46 -0700728 waitForBulkTransfer(dirMetadataDownloads.values(), /* cancelRemainingOnInterrupt=*/ true);
729
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700730 ImmutableMap.Builder<Path, DirectoryMetadata> directories = ImmutableMap.builder();
731 for (Map.Entry<Path, ListenableFuture<Tree>> metadataDownload :
732 dirMetadataDownloads.entrySet()) {
733 Path path = metadataDownload.getKey();
734 Tree directoryTree = getFromFuture(metadataDownload.getValue());
735 Map<Digest, Directory> childrenMap = new HashMap<>();
736 for (Directory childDir : directoryTree.getChildrenList()) {
737 childrenMap.put(digestUtil.compute(childDir), childDir);
738 }
739
740 directories.put(path, parseDirectory(path, directoryTree.getRoot(), childrenMap));
741 }
742
743 ImmutableMap.Builder<Path, FileMetadata> files = ImmutableMap.builder();
744 for (OutputFile outputFile : actionResult.getOutputFilesList()) {
745 files.put(
746 execRoot.getRelative(outputFile.getPath()),
747 new FileMetadata(
748 execRoot.getRelative(outputFile.getPath()),
749 outputFile.getDigest(),
750 outputFile.getIsExecutable()));
751 }
752
753 ImmutableMap.Builder<Path, SymlinkMetadata> symlinks = ImmutableMap.builder();
754 Iterable<OutputSymlink> outputSymlinks =
755 Iterables.concat(
756 actionResult.getOutputFileSymlinksList(),
757 actionResult.getOutputDirectorySymlinksList());
758 for (OutputSymlink symlink : outputSymlinks) {
759 symlinks.put(
760 execRoot.getRelative(symlink.getPath()),
761 new SymlinkMetadata(
762 execRoot.getRelative(symlink.getPath()), PathFragment.create(symlink.getTarget())));
763 }
764
765 return new ActionResultMetadata(files.build(), symlinks.build(), directories.build());
766 }
767
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700768 /** UploadManifest adds output metadata to a {@link ActionResult}. */
769 static class UploadManifest {
770 private final DigestUtil digestUtil;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800771 private final ActionResult.Builder result;
772 private final Path execRoot;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700773 private final boolean allowSymlinks;
olaolafbfb3cb2018-11-08 11:14:57 -0800774 private final boolean uploadSymlinks;
buchgr59b989e2019-08-06 01:36:23 -0700775 private final Map<Digest, Path> digestToFile = new HashMap<>();
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700776 private final Map<Digest, ByteString> digestToBlobs = new HashMap<>();
buchgr59b989e2019-08-06 01:36:23 -0700777 private Digest stderrDigest;
778 private Digest stdoutDigest;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800779
780 /**
781 * Create an UploadManifest from an ActionResult builder and an exec root. The ActionResult
782 * builder is populated through a call to {@link #addFile(Digest, Path)}.
783 */
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700784 public UploadManifest(
olaolafbfb3cb2018-11-08 11:14:57 -0800785 DigestUtil digestUtil,
786 ActionResult.Builder result,
787 Path execRoot,
788 boolean uploadSymlinks,
789 boolean allowSymlinks) {
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700790 this.digestUtil = digestUtil;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800791 this.result = result;
792 this.execRoot = execRoot;
olaolafbfb3cb2018-11-08 11:14:57 -0800793 this.uploadSymlinks = uploadSymlinks;
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700794 this.allowSymlinks = allowSymlinks;
buchgr59b989e2019-08-06 01:36:23 -0700795 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800796
buchgr59b989e2019-08-06 01:36:23 -0700797 public void setStdoutStderr(FileOutErr outErr) throws IOException {
798 if (outErr.getErrorPath().exists()) {
799 stderrDigest = digestUtil.compute(outErr.getErrorPath());
Jakob Buchgraberb7d300c2019-08-06 04:25:24 -0700800 digestToFile.put(stderrDigest, outErr.getErrorPath());
buchgr59b989e2019-08-06 01:36:23 -0700801 }
802 if (outErr.getOutputPath().exists()) {
803 stdoutDigest = digestUtil.compute(outErr.getOutputPath());
Jakob Buchgraberb7d300c2019-08-06 04:25:24 -0700804 digestToFile.put(stdoutDigest, outErr.getOutputPath());
buchgr59b989e2019-08-06 01:36:23 -0700805 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800806 }
807
808 /**
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700809 * Add a collection of files or directories to the UploadManifest. Adding a directory has the
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800810 * effect of 1) uploading a {@link Tree} protobuf message from which the whole structure of the
811 * directory, including the descendants, can be reconstructed and 2) uploading all the
812 * non-directory descendant files.
813 */
buchgrff008f42018-06-02 14:13:43 -0700814 public void addFiles(Collection<Path> files) throws ExecException, IOException {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800815 for (Path file : files) {
816 // TODO(ulfjack): Maybe pass in a SpawnResult here, add a list of output files to that, and
817 // rely on the local spawn runner to stat the files, instead of statting here.
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700818 FileStatus stat = file.statIfFound(Symlinks.NOFOLLOW);
olaolafbfb3cb2018-11-08 11:14:57 -0800819 // TODO(#6547): handle the case where the parent directory of the output file is an
820 // output symlink.
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700821 if (stat == null) {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800822 // We ignore requested results that have not been generated by the action.
823 continue;
824 }
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700825 if (stat.isDirectory()) {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800826 addDirectory(file);
olaolafbfb3cb2018-11-08 11:14:57 -0800827 } else if (stat.isFile() && !stat.isSpecialFile()) {
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700828 Digest digest = digestUtil.compute(file, stat.getSize());
buchgrfa36d2f2018-04-18 04:41:10 -0700829 addFile(digest, file);
olaolafbfb3cb2018-11-08 11:14:57 -0800830 } else if (stat.isSymbolicLink() && allowSymlinks) {
831 PathFragment target = file.readSymbolicLink();
832 // Need to resolve the symbolic link to know what to add, file or directory.
833 FileStatus statFollow = file.statIfFound(Symlinks.FOLLOW);
834 if (statFollow == null) {
835 throw new IOException(
836 String.format("Action output %s is a dangling symbolic link to %s ", file, target));
837 }
838 if (statFollow.isSpecialFile()) {
839 illegalOutput(file);
840 }
841 Preconditions.checkState(
842 statFollow.isFile() || statFollow.isDirectory(), "Unknown stat type for %s", file);
843 if (uploadSymlinks && !target.isAbsolute()) {
844 if (statFollow.isFile()) {
845 addFileSymbolicLink(file, target);
846 } else {
847 addDirectorySymbolicLink(file, target);
848 }
849 } else {
850 if (statFollow.isFile()) {
851 addFile(digestUtil.compute(file), file);
852 } else {
853 addDirectory(file);
854 }
855 }
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700856 } else {
857 illegalOutput(file);
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800858 }
859 }
860 }
861
olaolaf0aa55d2018-08-16 08:51:06 -0700862 /**
863 * Adds an action and command protos to upload. They need to be uploaded as part of the action
864 * result.
865 */
Jakob Buchgraber60566092019-11-11 06:28:22 -0800866 public void addAction(RemoteCacheClient.ActionKey actionKey, Action action, Command command) {
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700867 digestToBlobs.put(actionKey.getDigest(), action.toByteString());
868 digestToBlobs.put(action.getCommandDigest(), command.toByteString());
olaolaf0aa55d2018-08-16 08:51:06 -0700869 }
870
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800871 /** Map of digests to file paths to upload. */
872 public Map<Digest, Path> getDigestToFile() {
873 return digestToFile;
874 }
875
876 /**
877 * Map of digests to chunkers to upload. When the file is a regular, non-directory file it is
878 * transmitted through {@link #getDigestToFile()}. When it is a directory, it is transmitted as
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700879 * a {@link Tree} protobuf message through {@link #getDigestToBlobs()}.
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800880 */
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700881 public Map<Digest, ByteString> getDigestToBlobs() {
882 return digestToBlobs;
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800883 }
884
buchgr59b989e2019-08-06 01:36:23 -0700885 @Nullable
886 public Digest getStdoutDigest() {
887 return stdoutDigest;
888 }
889
890 @Nullable
891 public Digest getStderrDigest() {
892 return stderrDigest;
893 }
894
olaolafbfb3cb2018-11-08 11:14:57 -0800895 private void addFileSymbolicLink(Path file, PathFragment target) throws IOException {
896 result
897 .addOutputFileSymlinksBuilder()
898 .setPath(file.relativeTo(execRoot).getPathString())
899 .setTarget(target.toString());
900 }
901
902 private void addDirectorySymbolicLink(Path file, PathFragment target) throws IOException {
903 result
904 .addOutputDirectorySymlinksBuilder()
905 .setPath(file.relativeTo(execRoot).getPathString())
906 .setTarget(target.toString());
907 }
908
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800909 private void addFile(Digest digest, Path file) throws IOException {
910 result
911 .addOutputFilesBuilder()
912 .setPath(file.relativeTo(execRoot).getPathString())
913 .setDigest(digest)
914 .setIsExecutable(file.isExecutable());
915
916 digestToFile.put(digest, file);
917 }
918
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700919 private void addDirectory(Path dir) throws ExecException, IOException {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800920 Tree.Builder tree = Tree.newBuilder();
921 Directory root = computeDirectory(dir, tree);
922 tree.setRoot(root);
923
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700924 ByteString data = tree.build().toByteString();
925 Digest digest = digestUtil.compute(data.toByteArray());
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800926
927 if (result != null) {
928 result
929 .addOutputDirectoriesBuilder()
930 .setPath(dir.relativeTo(execRoot).getPathString())
931 .setTreeDigest(digest);
932 }
933
Jakob Buchgraber9fb83b42019-08-14 06:01:13 -0700934 digestToBlobs.put(digest, data);
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800935 }
936
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700937 private Directory computeDirectory(Path path, Tree.Builder tree)
938 throws ExecException, IOException {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800939 Directory.Builder b = Directory.newBuilder();
940
olaolafbfb3cb2018-11-08 11:14:57 -0800941 List<Dirent> sortedDirent = new ArrayList<>(path.readdir(Symlinks.NOFOLLOW));
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800942 sortedDirent.sort(Comparator.comparing(Dirent::getName));
943
944 for (Dirent dirent : sortedDirent) {
945 String name = dirent.getName();
946 Path child = path.getRelative(name);
947 if (dirent.getType() == Dirent.Type.DIRECTORY) {
948 Directory dir = computeDirectory(child, tree);
949 b.addDirectoriesBuilder().setName(name).setDigest(digestUtil.compute(dir));
950 tree.addChildren(dir);
olaolafbfb3cb2018-11-08 11:14:57 -0800951 } else if (dirent.getType() == Dirent.Type.SYMLINK && allowSymlinks) {
952 PathFragment target = child.readSymbolicLink();
953 if (uploadSymlinks && !target.isAbsolute()) {
954 // Whether it is dangling or not, we're passing it on.
955 b.addSymlinksBuilder().setName(name).setTarget(target.toString());
956 continue;
957 }
958 // Need to resolve the symbolic link now to know whether to upload a file or a directory.
959 FileStatus statFollow = child.statIfFound(Symlinks.FOLLOW);
960 if (statFollow == null) {
961 throw new IOException(
962 String.format(
963 "Action output %s is a dangling symbolic link to %s ", child, target));
964 }
965 if (statFollow.isFile() && !statFollow.isSpecialFile()) {
966 Digest digest = digestUtil.compute(child);
967 b.addFilesBuilder()
968 .setName(name)
969 .setDigest(digest)
970 .setIsExecutable(child.isExecutable());
971 digestToFile.put(digest, child);
972 } else if (statFollow.isDirectory()) {
973 Directory dir = computeDirectory(child, tree);
974 b.addDirectoriesBuilder().setName(name).setDigest(digestUtil.compute(dir));
975 tree.addChildren(dir);
976 } else {
977 illegalOutput(child);
978 }
979 } else if (dirent.getType() == Dirent.Type.FILE) {
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800980 Digest digest = digestUtil.compute(child);
981 b.addFilesBuilder().setName(name).setDigest(digest).setIsExecutable(child.isExecutable());
982 digestToFile.put(digest, child);
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700983 } else {
984 illegalOutput(child);
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -0800985 }
986 }
987
988 return b.build();
989 }
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700990
Jakob Buchgraber584ae242019-03-29 08:58:41 -0700991 private void illegalOutput(Path what) throws ExecException {
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700992 String kind = what.isSymbolicLink() ? "symbolic link" : "special file";
mschaller07933882020-06-24 14:38:23 -0700993 String message =
Benjamin Petersondd3ddb02018-05-03 09:20:08 -0700994 String.format(
995 "Output %s is a %s. Only regular files and directories may be "
996 + "uploaded to a remote cache. "
997 + "Change the file type or use --remote_allow_symlink_upload.",
mschaller07933882020-06-24 14:38:23 -0700998 what.relativeTo(execRoot), kind);
999 throw new UserExecException(createFailureDetail(message, Code.ILLEGAL_OUTPUT));
Benjamin Petersondd3ddb02018-05-03 09:20:08 -07001000 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001001 }
1002
1003 /** Release resources associated with the cache. The cache may not be used after calling this. */
1004 @Override
Jakob Buchgraber60566092019-11-11 06:28:22 -08001005 public void close() {
1006 cacheProtocol.close();
1007 }
buchgrff008f42018-06-02 14:13:43 -07001008
mschaller07933882020-06-24 14:38:23 -07001009 private static FailureDetail createFailureDetail(String message, Code detailedCode) {
1010 return FailureDetail.newBuilder()
1011 .setMessage(message)
1012 .setRemoteExecution(RemoteExecution.newBuilder().setCode(detailedCode))
1013 .build();
1014 }
1015
buchgrff008f42018-06-02 14:13:43 -07001016 /**
1017 * Creates an {@link OutputStream} that isn't actually opened until the first data is written.
1018 * This is useful to only have as many open file descriptors as necessary at a time to avoid
1019 * running into system limits.
1020 */
1021 private static class LazyFileOutputStream extends OutputStream {
1022
1023 private final Path path;
1024 private OutputStream out;
1025
1026 public LazyFileOutputStream(Path path) {
1027 this.path = path;
1028 }
1029
1030 @Override
1031 public void write(byte[] b) throws IOException {
1032 ensureOpen();
1033 out.write(b);
1034 }
1035
1036 @Override
1037 public void write(byte[] b, int off, int len) throws IOException {
1038 ensureOpen();
1039 out.write(b, off, len);
1040 }
1041
1042 @Override
1043 public void write(int b) throws IOException {
1044 ensureOpen();
1045 out.write(b);
1046 }
1047
1048 @Override
1049 public void flush() throws IOException {
1050 ensureOpen();
1051 out.flush();
1052 }
1053
1054 @Override
1055 public void close() throws IOException {
1056 ensureOpen();
1057 out.close();
1058 }
1059
1060 private void ensureOpen() throws IOException {
1061 if (out == null) {
1062 out = path.getOutputStream();
1063 }
1064 }
1065 }
Jakob Buchgraber584ae242019-03-29 08:58:41 -07001066
1067 /** In-memory representation of action result metadata. */
1068 static class ActionResultMetadata {
1069
1070 static class SymlinkMetadata {
1071 private final Path path;
1072 private final PathFragment target;
1073
1074 private SymlinkMetadata(Path path, PathFragment target) {
1075 this.path = path;
1076 this.target = target;
1077 }
1078
1079 public Path path() {
1080 return path;
1081 }
1082
1083 public PathFragment target() {
1084 return target;
1085 }
1086 }
1087
1088 static class FileMetadata {
1089 private final Path path;
1090 private final Digest digest;
1091 private final boolean isExecutable;
1092
1093 private FileMetadata(Path path, Digest digest, boolean isExecutable) {
1094 this.path = path;
1095 this.digest = digest;
1096 this.isExecutable = isExecutable;
1097 }
1098
1099 public Path path() {
1100 return path;
1101 }
1102
1103 public Digest digest() {
1104 return digest;
1105 }
1106
1107 public boolean isExecutable() {
1108 return isExecutable;
1109 }
1110 }
1111
1112 static class DirectoryMetadata {
1113 private final ImmutableList<FileMetadata> files;
1114 private final ImmutableList<SymlinkMetadata> symlinks;
1115
1116 private DirectoryMetadata(
1117 ImmutableList<FileMetadata> files, ImmutableList<SymlinkMetadata> symlinks) {
1118 this.files = files;
1119 this.symlinks = symlinks;
1120 }
1121
1122 public ImmutableList<FileMetadata> files() {
1123 return files;
1124 }
1125
1126 public ImmutableList<SymlinkMetadata> symlinks() {
1127 return symlinks;
1128 }
1129 }
1130
1131 private final ImmutableMap<Path, FileMetadata> files;
1132 private final ImmutableMap<Path, SymlinkMetadata> symlinks;
1133 private final ImmutableMap<Path, DirectoryMetadata> directories;
1134
1135 private ActionResultMetadata(
1136 ImmutableMap<Path, FileMetadata> files,
1137 ImmutableMap<Path, SymlinkMetadata> symlinks,
1138 ImmutableMap<Path, DirectoryMetadata> directories) {
1139 this.files = files;
1140 this.symlinks = symlinks;
1141 this.directories = directories;
1142 }
1143
1144 @Nullable
1145 public FileMetadata file(Path path) {
1146 return files.get(path);
1147 }
1148
1149 @Nullable
1150 public DirectoryMetadata directory(Path path) {
1151 return directories.get(path);
1152 }
1153
1154 public Collection<FileMetadata> files() {
1155 return files.values();
1156 }
1157
1158 public ImmutableSet<Entry<Path, DirectoryMetadata>> directories() {
1159 return directories.entrySet();
1160 }
1161
1162 public Collection<SymlinkMetadata> symlinks() {
1163 return symlinks.values();
1164 }
1165 }
Hadrien Chauvin3d0a04d2017-12-20 08:45:45 -08001166}