// Copyright 2016 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 com.google.common.collect.ImmutableMap; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import com.google.common.util.concurrent.ListenableFuture; import com.google.devtools.build.lib.actions.ActionExecutionContext; import com.google.devtools.build.lib.actions.ActionInput; import com.google.devtools.build.lib.actions.ActionInputFileCache; import com.google.devtools.build.lib.actions.ActionInputHelper; import com.google.devtools.build.lib.actions.ActionMetadata; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.ExecutionStrategy; import com.google.devtools.build.lib.actions.Executor; import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.actions.UserExecException; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.EventHandler; import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.io.FileOutErr; import com.google.devtools.build.lib.vfs.Path; import java.io.IOException; import java.nio.charset.Charset; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * Strategy that uses a distributed cache for sharing action input and output files. * Optionally this strategy also support offloading the work to a remote worker. */ @ExecutionStrategy( name = {"remote"}, contextType = SpawnActionContext.class ) final class RemoteSpawnStrategy implements SpawnActionContext { private final Path execRoot; private final StandaloneSpawnStrategy standaloneStrategy; private final RemoteActionCache remoteActionCache; private final RemoteWorkExecutor remoteWorkExecutor; RemoteSpawnStrategy( Map clientEnv, Path execRoot, RemoteOptions options, boolean verboseFailures, RemoteActionCache actionCache, RemoteWorkExecutor workExecutor) { this.execRoot = execRoot; this.standaloneStrategy = new StandaloneSpawnStrategy(execRoot, verboseFailures); this.remoteActionCache = actionCache; this.remoteWorkExecutor = workExecutor; } /** * Executes the given {@code spawn}. */ @Override public void exec(Spawn spawn, ActionExecutionContext actionExecutionContext) throws ExecException { if (!spawn.isRemotable()) { standaloneStrategy.exec(spawn, actionExecutionContext); return; } Executor executor = actionExecutionContext.getExecutor(); ActionMetadata actionMetadata = spawn.getResourceOwner(); ActionInputFileCache inputFileCache = actionExecutionContext.getActionInputFileCache(); EventHandler eventHandler = executor.getEventHandler(); // Compute a hash code to uniquely identify the action plus the action inputs. Hasher hasher = Hashing.sha256().newHasher(); // TODO(alpha): The action key is usually computed using the path to the tool and the // arguments. It does not take into account the content / version of the system tool (e.g. gcc). // Either I put information about the system tools in the hash or assume tools are always // checked in. Preconditions.checkNotNull(actionMetadata.getKey()); hasher.putString(actionMetadata.getKey(), Charset.defaultCharset()); List inputs = ActionInputHelper.expandArtifacts( spawn.getInputFiles(), actionExecutionContext.getArtifactExpander()); for (ActionInput input : inputs) { hasher.putString(input.getExecPathString(), Charset.defaultCharset()); try { // TODO(alpha): The digest from ActionInputFileCache is used to detect local file // changes. It might not be sufficient to identify the input file globally in the // remote action cache. Consider upgrading this to a better hash algorithm with // less collision. hasher.putBytes(inputFileCache.getDigest(input).toByteArray()); } catch (IOException e) { throw new UserExecException("Failed to get digest for input.", e); } } // Save the action output if found in the remote action cache. String actionOutputKey = hasher.hash().toString(); // Timeout for running the remote spawn. int timeout = 120; String timeoutStr = spawn.getExecutionInfo().get("timeout"); if (timeoutStr != null) { try { timeout = Integer.parseInt(timeoutStr); } catch (NumberFormatException e) { throw new UserExecException("could not parse timeout: ", e); } } try { // Look up action cache using |actionOutputKey|. Reuse the action output if it is found. if (writeActionOutput(spawn.getMnemonic(), actionOutputKey, eventHandler, true)) { return; } FileOutErr outErr = actionExecutionContext.getFileOutErr(); if (executeWorkRemotely( inputFileCache, spawn.getMnemonic(), actionOutputKey, spawn.getArguments(), inputs, spawn.getEnvironment(), spawn.getOutputFiles(), timeout, eventHandler, outErr)) { return; } // If nothing works then run spawn locally. standaloneStrategy.exec(spawn, actionExecutionContext); if (remoteActionCache != null) { remoteActionCache.putActionOutput(actionOutputKey, spawn.getOutputFiles()); } } catch (IOException e) { throw new UserExecException("Unexpected IO error.", e); } catch (UnsupportedOperationException e) { eventHandler.handle( Event.warn(spawn.getMnemonic() + " unsupported operation for action cache (" + e + ")")); } } /** * Submit work to execute remotely. * * @return True in case the action succeeded and all expected action outputs are found. */ private boolean executeWorkRemotely( ActionInputFileCache actionCache, String mnemonic, String actionOutputKey, List arguments, List inputs, ImmutableMap environment, Collection outputs, int timeout, EventHandler eventHandler, FileOutErr outErr) throws IOException { if (remoteWorkExecutor == null) { return false; } try { ListenableFuture future = remoteWorkExecutor.submit( execRoot, actionCache, actionOutputKey, arguments, inputs, environment, outputs, timeout); RemoteWorkExecutor.Response response = future.get(timeout, TimeUnit.SECONDS); if (!response.success()) { String exception = ""; if (!response.getException().isEmpty()) { exception = " (" + response.getException() + ")"; } eventHandler.handle( Event.warn( mnemonic + " failed to execute work remotely" + exception + ", running locally")); return false; } if (response.getOut() != null) { outErr.printOut(response.getOut()); } if (response.getErr() != null) { outErr.printErr(response.getErr()); } } catch (ExecutionException e) { eventHandler.handle( Event.warn(mnemonic + " failed to execute work remotely (" + e + "), running locally")); return false; } catch (TimeoutException e) { eventHandler.handle( Event.warn(mnemonic + " timed out executing work remotely (" + e + "), running locally")); return false; } catch (InterruptedException e) { Thread.currentThread().interrupt(); eventHandler.handle(Event.warn(mnemonic + " remote work interrupted (" + e + ")")); return false; } catch (WorkTooLargeException e) { eventHandler.handle(Event.warn(mnemonic + " cannot be run remotely (" + e + ")")); return false; } return writeActionOutput(mnemonic, actionOutputKey, eventHandler, false); } /** * Saves the action output from cache. Returns true if all action outputs are found. */ private boolean writeActionOutput( String mnemonic, String actionOutputKey, EventHandler eventHandler, boolean ignoreCacheNotFound) throws IOException { if (remoteActionCache == null) { return false; } try { remoteActionCache.writeActionOutput(actionOutputKey, execRoot); Event.info(mnemonic + " reuse action outputs from cache"); return true; } catch (CacheNotFoundException e) { if (!ignoreCacheNotFound) { eventHandler.handle( Event.warn(mnemonic + " some cache entries cannot be found (" + e + ")")); } } return false; } @Override public boolean willExecuteRemotely(boolean remotable) { // Returning true here just helps to estimate the cost of this computation is zero. return remotable; } @Override public boolean shouldPropagateExecException() { return false; } }