| // Copyright 2024 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.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.common.hash.Hashing.md5; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import build.bazel.remote.execution.v2.Digest; |
| import build.bazel.remote.execution.v2.DigestFunction; |
| import com.google.devtools.build.lib.actions.Action; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact; |
| import com.google.devtools.build.lib.actions.EnvironmentalExecException; |
| import com.google.devtools.build.lib.actions.ExecException; |
| import com.google.devtools.build.lib.actions.cache.OutputMetadataStore; |
| import com.google.devtools.build.lib.events.EventHandler; |
| import com.google.devtools.build.lib.profiler.Profiler; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.BatchStatRequest; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.BatchStatResponse; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.CleanRequest; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.CleanResponse; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.FinalizeArtifactsRequest; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.FinalizeArtifactsResponse; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.FinalizeBuildRequest; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.FinalizeBuildResponse; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.StageArtifactsRequest; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.StageArtifactsResponse; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.StartBuildRequest; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceProto.StartBuildResponse; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceREv2Proto.FileArtifactLocator; |
| import com.google.devtools.build.lib.remote.BazelOutputServiceREv2Proto.StartBuildArgs; |
| import com.google.devtools.build.lib.remote.RemoteExecutionService.ActionResultMetadata.FileMetadata; |
| import com.google.devtools.build.lib.remote.util.DigestUtil; |
| import com.google.devtools.build.lib.remote.util.Utils; |
| import com.google.devtools.build.lib.server.FailureDetails.Execution; |
| import com.google.devtools.build.lib.server.FailureDetails.Execution.Code; |
| import com.google.devtools.build.lib.server.FailureDetails.FailureDetail; |
| import com.google.devtools.build.lib.util.AbruptExitException; |
| import com.google.devtools.build.lib.util.DetailedExitCode; |
| import com.google.devtools.build.lib.vfs.BatchStat; |
| import com.google.devtools.build.lib.vfs.FileStatusWithDigest; |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; |
| import com.google.devtools.build.lib.vfs.ModifiedFileSet; |
| import com.google.devtools.build.lib.vfs.OutputService; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.lib.vfs.XattrProvider; |
| import com.google.devtools.build.lib.vfs.XattrProvider.DelegatingXattrProvider; |
| import com.google.protobuf.Any; |
| import io.grpc.Status; |
| import io.grpc.StatusRuntimeException; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.UUID; |
| import java.util.function.Supplier; |
| import javax.annotation.Nullable; |
| |
| /** Output service implementation for the remote build with local output service daemon. */ |
| public class BazelOutputService implements OutputService { |
| |
| private final String outputBaseId; |
| private final Supplier<Path> execRootSupplier; |
| private final Supplier<Path> outputPathSupplier; |
| private final DigestFunction.Value digestFunction; |
| private final String remoteCache; |
| private final String remoteInstanceName; |
| private final String remoteOutputServiceOutputPathPrefix; |
| private final boolean verboseFailures; |
| private final RemoteRetrier retrier; |
| private final ReferenceCountedChannel channel; |
| @Nullable private final String lastBuildId; |
| |
| @Nullable private String buildId; |
| @Nullable private PathFragment outputPathTarget; |
| |
| public BazelOutputService( |
| Path outputBase, |
| Supplier<Path> execRootSupplier, |
| Supplier<Path> outputPathSupplier, |
| DigestFunction.Value digestFunction, |
| String remoteCache, |
| String remoteInstanceName, |
| String remoteOutputServiceOutputPathPrefix, |
| boolean verboseFailures, |
| RemoteRetrier retrier, |
| ReferenceCountedChannel channel, |
| @Nullable String lastBuildId) { |
| this.outputBaseId = DigestUtil.hashCodeToString(md5().hashString(outputBase.toString(), UTF_8)); |
| this.execRootSupplier = execRootSupplier; |
| this.outputPathSupplier = outputPathSupplier; |
| this.digestFunction = digestFunction; |
| this.remoteCache = remoteCache; |
| this.remoteInstanceName = remoteInstanceName; |
| this.remoteOutputServiceOutputPathPrefix = remoteOutputServiceOutputPathPrefix; |
| this.verboseFailures = verboseFailures; |
| this.retrier = retrier; |
| this.channel = channel; |
| this.lastBuildId = lastBuildId; |
| } |
| |
| public void shutdown() { |
| channel.release(); |
| } |
| |
| @Override |
| public String getFileSystemName(String outputBaseFileSystemName) { |
| return "BazelOutputService"; |
| } |
| |
| private static void prepareOutputPath(Path outputPath, PathFragment target) |
| throws AbruptExitException { |
| // Plant a symlink at bazel-out pointing to the target returned from the remote output service. |
| try { |
| if (!outputPath.isSymbolicLink()) { |
| outputPath.deleteTree(); |
| } |
| FileSystemUtils.ensureSymbolicLink(outputPath, target); |
| } catch (IOException e) { |
| throw new AbruptExitException( |
| DetailedExitCode.of( |
| FailureDetail.newBuilder() |
| .setMessage( |
| String.format("Failed to plant output path symlink: %s", e.getMessage())) |
| .setExecution( |
| Execution.newBuilder().setCode(Code.LOCAL_OUTPUT_DIRECTORY_SYMLINK_FAILURE)) |
| .build()), |
| e); |
| } |
| } |
| |
| private static PathFragment constructOutputPathTarget( |
| PathFragment outputPathPrefix, StartBuildResponse response) throws AbruptExitException { |
| var outputPathSuffix = PathFragment.create(response.getOutputPathSuffix()); |
| if (outputPathPrefix.isEmpty() && !outputPathSuffix.isAbsolute()) { |
| throw new AbruptExitException( |
| DetailedExitCode.of( |
| FailureDetail.newBuilder() |
| .setMessage( |
| String.format( |
| "Expect StartBuildResponse.output_path_suffix to be an absolute path" |
| + " (because StartBuildRequest.output_path_prefix is empty), got %s.", |
| outputPathSuffix.isEmpty() |
| ? "an empty string" |
| : response.getOutputPathSuffix())) |
| .setExecution(Execution.newBuilder().setCode(Code.EXECUTION_UNKNOWN)) |
| .build())); |
| } else if (outputPathSuffix.isAbsolute()) { |
| throw new AbruptExitException( |
| DetailedExitCode.of( |
| FailureDetail.newBuilder() |
| .setMessage( |
| String.format( |
| "Expect StartBuildResponse.output_path_suffix to be a relative path, got" |
| + " %s.", |
| response.getOutputPathSuffix())) |
| .setExecution(Execution.newBuilder().setCode(Code.EXECUTION_UNKNOWN)) |
| .build())); |
| } else if (outputPathSuffix.containsUplevelReferences()) { |
| throw new AbruptExitException( |
| DetailedExitCode.of( |
| FailureDetail.newBuilder() |
| .setMessage( |
| String.format( |
| "Expect normalized StartBuildResponse.output_path_suffix to not contain" |
| + " uplevel references, got %s.", |
| outputPathSuffix)) |
| .setExecution(Execution.newBuilder().setCode(Code.EXECUTION_UNKNOWN)) |
| .build())); |
| } |
| |
| var outputPathTarget = outputPathPrefix.getRelative(outputPathSuffix); |
| checkState(outputPathTarget.isAbsolute()); |
| return outputPathTarget; |
| } |
| |
| @Override |
| public ModifiedFileSet startBuild( |
| UUID buildId, String workspaceName, EventHandler eventHandler, boolean finalizeActions) |
| throws AbruptExitException, InterruptedException { |
| checkState(this.buildId == null, "this.buildId must be null"); |
| this.buildId = buildId.toString(); |
| var outputPathPrefix = PathFragment.create(remoteOutputServiceOutputPathPrefix); |
| if (!outputPathPrefix.isEmpty() && !outputPathPrefix.isAbsolute()) { |
| throw new AbruptExitException( |
| DetailedExitCode.of( |
| FailureDetail.newBuilder() |
| .setMessage( |
| String.format( |
| "--experimental_remote_output_service_path_prefix must be an absolute" |
| + " path, got '%s'", |
| outputPathPrefix)) |
| .setExecution(Execution.newBuilder().setCode(Code.EXECUTION_UNKNOWN)) |
| .build())); |
| } |
| var outputPath = outputPathSupplier.get(); |
| |
| // Notify the remote output service that the build is about to start. The remote output service |
| // will return the directory in which it wants us to let the build take place. |
| var request = |
| StartBuildRequest.newBuilder() |
| .setVersion(1) |
| .setOutputBaseId(outputBaseId) |
| .setBuildId(this.buildId) |
| .setArgs( |
| Any.pack( |
| StartBuildArgs.newBuilder() |
| .setRemoteCache(remoteCache) |
| .setInstanceName(remoteInstanceName) |
| .setDigestFunction(digestFunction) |
| .build())) |
| .setOutputPathPrefix(outputPathPrefix.toString()) |
| .putOutputPathAliases(outputPath.toString(), ".") |
| .build(); |
| |
| StartBuildResponse response; |
| try { |
| response = startBuild(request); |
| } catch (IOException e) { |
| throw new AbruptExitException( |
| DetailedExitCode.of( |
| FailureDetail.newBuilder() |
| .setMessage( |
| String.format( |
| "StartBuild failed: %s", Utils.grpcAwareErrorMessage(e, verboseFailures))) |
| .setExecution(Execution.newBuilder().setCode(Code.EXECUTION_UNKNOWN)) |
| .build())); |
| } |
| |
| checkState(outputPathTarget == null, "outputPathTarget must be null"); |
| outputPathTarget = constructOutputPathTarget(outputPathPrefix, response); |
| prepareOutputPath(outputPath, outputPathTarget); |
| |
| if (finalizeActions && response.hasInitialOutputPathContents()) { |
| var initialOutputPathContents = response.getInitialOutputPathContents(); |
| if (!initialOutputPathContents.getBuildId().equals(lastBuildId)) { |
| return ModifiedFileSet.EVERYTHING_DELETED; |
| } |
| |
| // TODO(chiwang): Handle StartBuildResponse.initial_output_path_contents |
| } |
| |
| return ModifiedFileSet.EVERYTHING_MODIFIED; |
| } |
| |
| private StartBuildResponse startBuild(StartBuildRequest request) |
| throws IOException, InterruptedException { |
| return retrier.execute( |
| () -> |
| channel.withChannelBlocking( |
| channel -> { |
| try (var sc = Profiler.instance().profile("BazelOutputService.StartBuild")) { |
| return BazelOutputServiceGrpc.newBlockingStub(channel).startBuild(request); |
| } catch (StatusRuntimeException e) { |
| throw new IOException(e); |
| } |
| })); |
| } |
| |
| protected void stageArtifacts(List<FileMetadata> files) throws IOException, InterruptedException { |
| var outputPath = outputPathSupplier.get(); |
| var request = StageArtifactsRequest.newBuilder(); |
| request.setBuildId(buildId); |
| for (var file : files) { |
| request.addArtifacts( |
| StageArtifactsRequest.Artifact.newBuilder() |
| .setPath(file.path().relativeTo(outputPath).toString()) |
| .setLocator( |
| Any.pack(FileArtifactLocator.newBuilder().setDigest(file.digest()).build())) |
| .build()); |
| } |
| var response = stageArtifacts(request.build()); |
| if (response.getResponsesCount() != files.size()) { |
| throw new IOException( |
| String.format( |
| "StageArtifacts failed: expect %s responses from StageArtifactsResponse, got %s", |
| files.size(), response.getResponsesCount())); |
| } |
| |
| for (var i = 0; i < files.size(); ++i) { |
| var fileResponse = response.getResponses(i); |
| if (fileResponse.getStatus().getCode() != Status.Code.OK.value()) { |
| throw new IOException( |
| String.format( |
| "Failed to stage %s, code: %s", |
| files.get(i).path().relativeTo(outputPath), fileResponse.getStatus())); |
| } |
| } |
| } |
| |
| private StageArtifactsResponse stageArtifacts(StageArtifactsRequest request) |
| throws IOException, InterruptedException { |
| return retrier.execute( |
| () -> |
| channel.withChannelBlocking( |
| channel -> { |
| try (var sc = Profiler.instance().profile("BazelOutputService.StageArtifacts")) { |
| return BazelOutputServiceGrpc.newBlockingStub(channel).stageArtifacts(request); |
| } catch (StatusRuntimeException e) { |
| throw new IOException(e); |
| } |
| })); |
| } |
| |
| @Override |
| public void finalizeBuild(boolean buildSuccessful) |
| throws AbruptExitException, InterruptedException { |
| var request = |
| FinalizeBuildRequest.newBuilder() |
| .setBuildId(checkNotNull(buildId)) |
| .setBuildSuccessful(buildSuccessful) |
| .build(); |
| try { |
| var unused = finalizeBuild(request); |
| } catch (IOException e) { |
| throw new AbruptExitException( |
| DetailedExitCode.of( |
| FailureDetail.newBuilder() |
| .setMessage( |
| String.format( |
| "FinalizeBuild failed: %s", |
| Utils.grpcAwareErrorMessage(e, verboseFailures))) |
| .setExecution(Execution.newBuilder().setCode(Code.EXECUTION_UNKNOWN)) |
| .build())); |
| } finally { |
| this.buildId = null; |
| this.outputPathTarget = null; |
| } |
| } |
| |
| private FinalizeBuildResponse finalizeBuild(FinalizeBuildRequest request) |
| throws IOException, InterruptedException { |
| return retrier.execute( |
| () -> |
| channel.withChannelBlocking( |
| channel -> { |
| try (var sc = Profiler.instance().profile("BazelOutputService.FinalizeBuild")) { |
| return BazelOutputServiceGrpc.newBlockingStub(channel).finalizeBuild(request); |
| } catch (StatusRuntimeException e) { |
| throw new IOException(e); |
| } |
| })); |
| } |
| |
| @Override |
| public void finalizeAction(Action action, OutputMetadataStore outputMetadataStore) |
| throws IOException, EnvironmentalExecException, InterruptedException { |
| var execRoot = execRootSupplier.get(); |
| var outputPath = outputPathSupplier.get(); |
| |
| var request = FinalizeArtifactsRequest.newBuilder(); |
| request.setBuildId(buildId); |
| for (var output : action.getOutputs()) { |
| if (outputMetadataStore.artifactOmitted(output)) { |
| continue; |
| } |
| |
| if (output.isTreeArtifact()) { |
| // TODO(chiwang): Use TreeArtifactLocator |
| var children = outputMetadataStore.getTreeArtifactChildren((SpecialArtifact) output); |
| for (var child : children) { |
| addArtifact(outputMetadataStore, execRoot, outputPath, request, child); |
| } |
| } else { |
| addArtifact(outputMetadataStore, execRoot, outputPath, request, output); |
| } |
| } |
| |
| var unused = finalizeArtifacts(request.build()); |
| } |
| |
| private FinalizeArtifactsResponse finalizeArtifacts(FinalizeArtifactsRequest request) |
| throws IOException, InterruptedException { |
| return retrier.execute( |
| () -> |
| channel.withChannelBlocking( |
| channel -> { |
| try (var sc = |
| Profiler.instance().profile("BazelOutputService.FinalizeArtifacts")) { |
| return BazelOutputServiceGrpc.newBlockingStub(channel) |
| .finalizeArtifacts(request); |
| } catch (StatusRuntimeException e) { |
| throw new IOException(e); |
| } |
| })); |
| } |
| |
| private static void addArtifact( |
| OutputMetadataStore outputMetadataStore, |
| Path execRoot, |
| Path outputPath, |
| FinalizeArtifactsRequest.Builder builder, |
| Artifact output) |
| throws IOException, InterruptedException { |
| checkState(!output.isTreeArtifact()); |
| var metadata = outputMetadataStore.getOutputMetadata(output); |
| if (metadata.getType().isFile()) { |
| var digest = DigestUtil.buildDigest(metadata.getDigest(), metadata.getSize()); |
| var path = execRoot.getRelative(output.getExecPath()).relativeTo(outputPath).toString(); |
| builder.addArtifacts( |
| FinalizeArtifactsRequest.Artifact.newBuilder() |
| .setPath(path) |
| .setLocator(Any.pack(FileArtifactLocator.newBuilder().setDigest(digest).build())) |
| .build()); |
| } |
| } |
| |
| private record BazelOutputServiceFile(Digest digest) implements FileStatusWithDigest { |
| @Override |
| public boolean isFile() { |
| return true; |
| } |
| |
| @Override |
| public boolean isDirectory() { |
| return false; |
| } |
| |
| @Override |
| public boolean isSymbolicLink() { |
| return false; |
| } |
| |
| @Override |
| public boolean isSpecialFile() { |
| return false; |
| } |
| |
| @Override |
| public long getSize() { |
| return digest.getSizeBytes(); |
| } |
| |
| @Override |
| public long getLastModifiedTime() { |
| throw new UnsupportedOperationException("Cannot get last modified time"); |
| } |
| |
| @Override |
| public long getLastChangeTime() { |
| throw new UnsupportedOperationException("Cannot get last change time"); |
| } |
| |
| @Override |
| public long getNodeId() { |
| throw new UnsupportedOperationException("Cannot get node id"); |
| } |
| |
| @Nullable |
| @Override |
| public byte[] getDigest() { |
| return DigestUtil.toBinaryDigest(digest); |
| } |
| } |
| |
| private record BazelOutputServiceSymlink(String target) implements FileStatusWithDigest { |
| @Override |
| public boolean isFile() { |
| return false; |
| } |
| |
| @Override |
| public boolean isDirectory() { |
| return false; |
| } |
| |
| @Override |
| public boolean isSymbolicLink() { |
| return true; |
| } |
| |
| @Override |
| public boolean isSpecialFile() { |
| return false; |
| } |
| |
| @Override |
| public long getSize() { |
| throw new UnsupportedOperationException("Cannot get size"); |
| } |
| |
| @Override |
| public long getLastModifiedTime() { |
| throw new UnsupportedOperationException("Cannot get last modified time"); |
| } |
| |
| @Override |
| public long getLastChangeTime() { |
| throw new UnsupportedOperationException("Cannot get last change time"); |
| } |
| |
| @Override |
| public long getNodeId() { |
| throw new UnsupportedOperationException("Cannot get node id"); |
| } |
| |
| @Nullable |
| @Override |
| public byte[] getDigest() { |
| throw new UnsupportedOperationException("Cannot get digest"); |
| } |
| } |
| |
| private record BazelOutputServiceDirectory() implements FileStatusWithDigest { |
| @Override |
| public boolean isFile() { |
| return false; |
| } |
| |
| @Override |
| public boolean isDirectory() { |
| return true; |
| } |
| |
| @Override |
| public boolean isSymbolicLink() { |
| return false; |
| } |
| |
| @Override |
| public boolean isSpecialFile() { |
| return false; |
| } |
| |
| @Override |
| public long getSize() { |
| throw new UnsupportedOperationException("Cannot get size"); |
| } |
| |
| @Override |
| public long getLastModifiedTime() { |
| return 0; |
| } |
| |
| @Override |
| public long getLastChangeTime() { |
| throw new UnsupportedOperationException("Cannot get last change time"); |
| } |
| |
| @Override |
| public long getNodeId() { |
| throw new UnsupportedOperationException("Cannot get node id"); |
| } |
| |
| @Nullable |
| @Override |
| public byte[] getDigest() { |
| throw new UnsupportedOperationException("Cannot get digest"); |
| } |
| } |
| |
| @Override |
| public BatchStat getBatchStatter() { |
| return paths -> { |
| var outputPath = outputPathSupplier.get().asFragment(); |
| var execRoot = execRootSupplier.get(); |
| |
| var request = BatchStatRequest.newBuilder(); |
| request.setBuildId(checkNotNull(buildId)); |
| |
| var unsupportedPathIndexSet = new HashSet<Integer>(); |
| int index = 0; |
| for (var execPath : paths) { |
| String pathString = null; |
| var path = execRoot.getRelative(execPath).asFragment(); |
| if (path.startsWith(outputPath)) { |
| pathString = path.relativeTo(outputPath).toString(); |
| } else if (path.startsWith(checkNotNull(outputPathTarget))) { |
| pathString = path.relativeTo(outputPathTarget).toString(); |
| } |
| |
| if (pathString == null) { |
| unsupportedPathIndexSet.add(index); |
| } else { |
| request.addPaths(pathString); |
| } |
| ++index; |
| } |
| |
| var response = BazelOutputService.this.batchStat(request.build()); |
| if (response.getResponsesCount() != request.getPathsCount()) { |
| throw new IOException( |
| String.format( |
| "BatchStat failed: expect %s responses, got %s", |
| request.getPathsCount(), response.getResponsesCount())); |
| } |
| |
| var result = new ArrayList<FileStatusWithDigest>(index); |
| for (int i = 0; i < index; ++i) { |
| if (unsupportedPathIndexSet.contains(i)) { |
| result.add(null); |
| continue; |
| } |
| |
| var statResponse = response.getResponses(i); |
| if (!statResponse.hasStat()) { |
| result.add(null); |
| continue; |
| } |
| |
| var stat = statResponse.getStat(); |
| if (stat.hasFile() && stat.getFile().hasLocator()) { |
| var locator = stat.getFile().getLocator(); |
| result.add( |
| new BazelOutputServiceFile(locator.unpack(FileArtifactLocator.class).getDigest())); |
| } else if (stat.hasSymlink()) { |
| // TODO(chiwang): The target is currently unused by the call site, instead it resolves the |
| // symlink manually. Optimize it. |
| result.add(new BazelOutputServiceSymlink(stat.getSymlink().getTarget())); |
| } else if (stat.hasDirectory()) { |
| result.add(new BazelOutputServiceDirectory()); |
| } else { |
| result.add(null); |
| } |
| } |
| return result; |
| }; |
| } |
| |
| @Override |
| public boolean canCreateSymlinkTree() { |
| return false; |
| } |
| |
| @Override |
| public void createSymlinkTree( |
| Map<PathFragment, PathFragment> symlinks, PathFragment symlinkTreeRoot) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public void clean() throws ExecException, InterruptedException { |
| var request = CleanRequest.newBuilder().setOutputBaseId(outputBaseId).build(); |
| try { |
| var unused = clean(request); |
| } catch (IOException e) { |
| throw new EnvironmentalExecException(e, Code.UNEXPECTED_EXCEPTION); |
| } |
| } |
| |
| private CleanResponse clean(CleanRequest request) throws IOException, InterruptedException { |
| return retrier.execute( |
| () -> |
| channel.withChannelBlocking( |
| channel -> { |
| try (var sc = Profiler.instance().profile("BazelOutputService.Clean")) { |
| return BazelOutputServiceGrpc.newBlockingStub(channel).clean(request); |
| } catch (StatusRuntimeException e) { |
| throw new IOException(e); |
| } |
| })); |
| } |
| |
| private BatchStatResponse batchStat(BatchStatRequest request) |
| throws IOException, InterruptedException { |
| return retrier.execute( |
| () -> |
| channel.withChannelBlocking( |
| channel -> { |
| try (var sc = Profiler.instance().profile("BazelOutputService.BatchStat")) { |
| return BazelOutputServiceGrpc.newBlockingStub(channel).batchStat(request); |
| } catch (StatusRuntimeException e) { |
| throw new IOException(e); |
| } |
| })); |
| } |
| |
| @Override |
| public XattrProvider getXattrProvider(XattrProvider delegate) { |
| return new DelegatingXattrProvider(delegate) { |
| @Nullable |
| @Override |
| public byte[] getFastDigest(Path path) throws IOException { |
| var outputPath = outputPathSupplier.get(); |
| var buildId = checkNotNull(BazelOutputService.this.buildId); |
| var outputPathTarget = checkNotNull(BazelOutputService.this.outputPathTarget); |
| |
| String pathString = null; |
| if (path.startsWith(outputPath)) { |
| pathString = path.relativeTo(outputPath).toString(); |
| } else if (path.startsWith(outputPathTarget)) { |
| pathString = path.asFragment().relativeTo(outputPathTarget).toString(); |
| } |
| if (pathString == null) { |
| return super.getFastDigest(path); |
| } |
| |
| var request = |
| BatchStatRequest.newBuilder().setBuildId(buildId).addPaths(pathString).build(); |
| BatchStatResponse response; |
| try { |
| response = batchStat(request); |
| } catch (InterruptedException e) { |
| throw new IOException(e); |
| } |
| |
| if (response.getResponsesCount() != 1) { |
| throw new IOException( |
| String.format( |
| "BatchStat failed: expect 1 response, got %s", response.getResponsesCount())); |
| } |
| |
| var statResponse = response.getResponses(0); |
| if (!statResponse.hasStat()) { |
| throw new FileNotFoundException(path.getPathString()); |
| } |
| |
| var stat = statResponse.getStat(); |
| if (stat.hasFile()) { |
| var file = stat.getFile(); |
| if (file.hasLocator()) { |
| var locator = file.getLocator().unpack(FileArtifactLocator.class); |
| return DigestUtil.toBinaryDigest(locator.getDigest()); |
| } |
| } |
| |
| return null; |
| } |
| }; |
| } |
| } |