blob: 854c3e66017724c4e4a48f5e8103d8e79fcc3c17 [file]
// Copyright 2026 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.remote;
import static com.google.devtools.build.lib.remote.util.Futures.getFromFuture;
import build.bazel.remote.execution.v2.Digest;
import build.bazel.remote.execution.v2.RequestMetadata;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.AbstractAction;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.EnvironmentalExecException;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.RunningActionEvent;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.analysis.actions.FileWriteActionContext;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext;
import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext.CachePolicy;
import com.google.devtools.build.lib.remote.common.RemoteCacheClient.Blob;
import com.google.devtools.build.lib.remote.util.DigestUtil;
import com.google.devtools.build.lib.remote.util.TracingMetadataUtils;
import com.google.devtools.build.lib.remote.util.Utils;
import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
import com.google.devtools.build.lib.util.DeterministicWriter;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
/**
* A {@link FileWriteActionContext} that stores the contents of the file in the disk and/or remote
* cache and records them as a remote output instead of writing them to disk.
*
* <p>The file is written to disk instead if the file write isn't remotable, if the action doesn't
* run on a {@link RemoteActionFileSystem} or if the output has to be downloaded anyway according to
* {@code --remote_download_outputs} or {@code --remote_download_regex}.
*
* <p>The contents are always stored in the disk cache if there is one. They are uploaded to the
* remote cache if it doesn't have them yet and uploads are enabled. If there is no disk cache and
* uploads to the remote cache are disabled, the contents are only recorded as a remote output if
* the remote cache already has them and the file is written to disk otherwise. If the caches lose
* the contents later, action rewinding re-executes the action to store them again.
*/
public final class RemoteFileWriteStrategy implements FileWriteActionContext {
private final FileWriteActionContext localStrategy;
private final CombinedCache combinedCache;
private final RemoteOutputChecker remoteOutputChecker;
private final DigestUtil digestUtil;
private final String buildRequestId;
private final String commandId;
private final Duration remoteCacheTtl;
private final boolean remoteUploadEnabled;
private final boolean verboseFailures;
public RemoteFileWriteStrategy(
FileWriteActionContext localStrategy,
CombinedCache combinedCache,
RemoteOutputChecker remoteOutputChecker,
DigestUtil digestUtil,
String buildRequestId,
String commandId,
Duration remoteCacheTtl,
boolean remoteUploadEnabled,
boolean verboseFailures) {
this.localStrategy = localStrategy;
this.combinedCache = combinedCache;
this.remoteOutputChecker = remoteOutputChecker;
this.digestUtil = digestUtil;
this.buildRequestId = buildRequestId;
this.commandId = commandId;
this.remoteCacheTtl = remoteCacheTtl;
this.remoteUploadEnabled = remoteUploadEnabled;
this.verboseFailures = verboseFailures;
}
@Override
public ImmutableList<SpawnResult> writeOutputToFile(
AbstractAction action,
ActionExecutionContext actionExecutionContext,
DeterministicWriter deterministicWriter,
boolean makeExecutable,
boolean isRemotable,
Artifact output)
throws InterruptedException, ExecException {
// Non-remotable file writes are consumed by Bazel itself and thus have to exist locally.
// Outputs that are requested for download would be materialized right after the action anyway.
if (!isRemotable
|| !(actionExecutionContext.getActionFileSystem()
instanceof RemoteActionFileSystem remoteActionFileSystem)
|| remoteOutputChecker.shouldDownloadOutput(
output.getExecPath(), /* treeRootExecPath= */ null)) {
return localStrategy.writeOutputToFile(
action, actionExecutionContext, deterministicWriter, makeExecutable, isRemotable, output);
}
actionExecutionContext.getEventHandler().post(new RunningActionEvent(action, "remote"));
try (var _ = Profiler.instance().profile("RemoteFileWriteStrategy.writeOutputToFile")) {
Digest digest;
try {
digest = digestUtil.compute(deterministicWriter);
} catch (IOException e) {
throw new EnvironmentalExecException(e, Code.FILE_WRITE_IO_EXCEPTION);
}
var context = RemoteActionExecutionContext.create(buildRequestMetadata(action));
try {
if (!storeInCaches(context, digest, deterministicWriter, output)) {
// No cache has the contents and Bazel isn't allowed to store them in any, so the file
// has to exist locally for consumers to be able to use it.
return localStrategy.writeOutputToFile(
action,
actionExecutionContext,
deterministicWriter,
makeExecutable,
isRemotable,
output);
}
} catch (IOException e) {
// Cache failures shouldn't fail the build, so write the file to disk instead.
actionExecutionContext
.getEventHandler()
.handle(Event.warn("Remote Cache: " + Utils.grpcAwareErrorMessage(e, verboseFailures)));
return localStrategy.writeOutputToFile(
action,
actionExecutionContext,
deterministicWriter,
makeExecutable,
isRemotable,
output);
}
// TODO: Bazel currently marks all output files as executable after local execution and
// stages all files as executable for remote execution, so the executable bit isn't tracked
// in the metadata yet.
try {
remoteActionFileSystem.injectRemoteFile(
actionExecutionContext.getInputPath(output).asFragment(),
DigestUtil.toBinaryDigest(digest),
digest.getSizeBytes(),
Instant.now().plus(remoteCacheTtl),
/* inMemoryOutput= */ false);
} catch (IOException e) {
throw new EnvironmentalExecException(e, Code.FILE_WRITE_IO_EXCEPTION);
}
}
return ImmutableList.of();
}
/**
* Stores the contents in every cache that Bazel is allowed to write to and that doesn't have them
* yet.
*
* @return whether at least one cache has the contents afterwards
*/
private boolean storeInCaches(
RemoteActionExecutionContext context,
Digest digest,
DeterministicWriter deterministicWriter,
Artifact output)
throws IOException, InterruptedException {
// The disk cache is always writable and skips the write if it already has the contents.
boolean writeToDiskCache = combinedCache.hasDiskCache();
boolean presentInRemoteCache = false;
boolean uploadToRemoteCache = false;
if (combinedCache.hasRemoteCache()) {
ImmutableSet<Digest> missingDigests =
getFromFuture(combinedCache.findMissingDigests(context, ImmutableList.of(digest)));
presentInRemoteCache = missingDigests.isEmpty();
uploadToRemoteCache = !presentInRemoteCache && remoteUploadEnabled;
}
if (writeToDiskCache || uploadToRemoteCache) {
getFromFuture(
combinedCache.uploadBlob(
context.withWriteCachePolicy(
CachePolicy.create(uploadToRemoteCache, writeToDiskCache)),
digest,
new DeterministicWriterBlob(deterministicWriter, output),
// The remote cache just reported the blob as missing.
/* force= */ true));
}
return writeToDiskCache || presentInRemoteCache || uploadToRemoteCache;
}
private RequestMetadata buildRequestMetadata(AbstractAction action) {
ActionOwner owner = action.getOwner();
return TracingMetadataUtils.buildMetadata(
buildRequestId,
commandId,
action.getPrimaryOutput().getExecPathString(),
action.getMnemonic(),
owner.getLabel() != null ? owner.getLabel().getCanonicalForm() : null,
owner.getConfigurationChecksum());
}
/** Streams the contents of a {@link DeterministicWriter} through a bounded pipe. */
private record DeterministicWriterBlob(DeterministicWriter writer, Artifact output)
implements Blob {
@Override
public InputStream get() {
return writer.getInputStream(Chunker.getDefaultChunkSize());
}
@Override
public String description() {
return output.getExecPathString();
}
}
}