blob: b5f4326ba23b79f644ff7d4578374296544f8f34 [file] [log] [blame]
// 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<String, String> 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<ActionInput> 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<String> arguments,
List<ActionInput> inputs,
ImmutableMap<String, String> environment,
Collection<? extends ActionInput> outputs,
int timeout,
EventHandler eventHandler,
FileOutErr outErr)
throws IOException {
if (remoteWorkExecutor == null) {
return false;
}
try {
ListenableFuture<RemoteWorkExecutor.Response> 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;
}
}