blob: db9b7752786a6a57de259aa1b490117b2029e875 [file] [log] [blame]
// Copyright 2018 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.dynamic;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.eventbus.Subscribe;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.SpawnStrategy;
import com.google.devtools.build.lib.actions.Spawns;
import com.google.devtools.build.lib.buildtool.BuildResult;
import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
import com.google.devtools.build.lib.concurrent.ExecutorUtil;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.exec.ExecutionPolicy;
import com.google.devtools.build.lib.exec.SpawnStrategyRegistry;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.server.FailureDetails.ExecutionOptions;
import com.google.devtools.build.lib.server.FailureDetails.ExecutionOptions.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.common.options.OptionsBase;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** {@link BlazeModule} providing support for dynamic spawn execution and scheduling. */
public class DynamicExecutionModule extends BlazeModule {
private ExecutorService executorService;
/**
* If true, this is the first build since this server started (excluding failed builds). This
* allows turning off dynamic execution for an initial build, avoiding dynamic execution on most
* clean builds.
*/
private static boolean firstBuild = true;
public DynamicExecutionModule() {}
@VisibleForTesting
DynamicExecutionModule(ExecutorService executorService) {
this.executorService = executorService;
}
@Override
public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
return "build".equals(command.name())
? ImmutableList.of(DynamicExecutionOptions.class)
: ImmutableList.<Class<? extends OptionsBase>>of();
}
@Override
public void beforeCommand(CommandEnvironment env) {
executorService =
Executors.newCachedThreadPool(
new ThreadFactoryBuilder().setNameFormat("dynamic-execution-thread-%d").build());
env.getEventBus().register(this);
}
@VisibleForTesting
ImmutableMap<String, List<String>> getLocalStrategies(DynamicExecutionOptions options)
throws AbruptExitException {
// Options that set "allowMultiple" to true ignore the default value, so we replicate that
// functionality here. Additionally, since we are still supporting --dynamic_worker_strategy,
// but will deprecate it soon, we add its functionality to --dynamic_local_strategy. This allows
// users to set --dynamic_local_strategy and not --dynamic_worker_strategy to stop defaulting to
// worker strategy. For simplicity, we add the default strategy first, it may be overridden
// later.
// ImmutableMap.Builder fails on duplicates, so we use a regular map first to remove dups.
Map<String, List<String>> localAndWorkerStrategies = new HashMap<>();
// TODO(steinman): Deprecate --dynamic_worker_strategy and clean this up.
List<String> defaultValue = Lists.newArrayList();
String workerStrategy =
options.dynamicWorkerStrategy.isEmpty() ? "worker" : options.dynamicWorkerStrategy;
defaultValue.addAll(ImmutableList.of(workerStrategy, "sandboxed"));
throwIfContainsDynamic(defaultValue, "--dynamic_local_strategy");
localAndWorkerStrategies.put("", defaultValue);
if (!options.dynamicLocalStrategy.isEmpty()) {
for (Map.Entry<String, List<String>> entry : options.dynamicLocalStrategy) {
if ("".equals(entry.getKey())) {
List<String> newValue = Lists.newArrayList();
if (!options.dynamicWorkerStrategy.isEmpty()) {
newValue.add(options.dynamicWorkerStrategy);
}
newValue.addAll(entry.getValue());
localAndWorkerStrategies.put("", newValue);
} else {
localAndWorkerStrategies.put(entry.getKey(), entry.getValue());
}
throwIfContainsDynamic(entry.getValue(), "--dynamic_local_strategy");
}
}
return ImmutableMap.copyOf(localAndWorkerStrategies);
}
private ImmutableMap<String, List<String>> getRemoteStrategies(DynamicExecutionOptions options)
throws AbruptExitException {
Map<String, List<String>> strategies = new HashMap<>(); // Needed to dedup
for (Map.Entry<String, List<String>> e : options.dynamicRemoteStrategy) {
throwIfContainsDynamic(e.getValue(), "--dynamic_remote_strategy");
strategies.put(e.getKey(), e.getValue());
}
return options.dynamicRemoteStrategy.isEmpty()
? ImmutableMap.of("", ImmutableList.of("remote"))
: ImmutableMap.copyOf(strategies);
}
@Override
public void registerSpawnStrategies(
SpawnStrategyRegistry.Builder registryBuilder, CommandEnvironment env)
throws AbruptExitException {
registerSpawnStrategies(
registryBuilder,
env.getOptions().getOptions(DynamicExecutionOptions.class),
env.getReporter());
}
// CommandEnvironment is difficult to access in tests, so use this method for testing.
@VisibleForTesting
final void registerSpawnStrategies(
SpawnStrategyRegistry.Builder registryBuilder,
DynamicExecutionOptions options,
Reporter reporter)
throws AbruptExitException {
if (!options.internalSpawnScheduler) {
return;
}
SpawnStrategy strategy =
new DynamicSpawnStrategy(
executorService,
options,
this::getExecutionPolicy,
this::getPostProcessingSpawnForLocalExecution);
registryBuilder.registerStrategy(strategy, "dynamic", "dynamic_worker");
if (firstBuild && options.skipFirstBuild) {
reporter.handle(
Event.info("Disabling dynamic execution until we have seen a successful build"));
} else {
registryBuilder.addDynamicLocalStrategies(getLocalStrategies(options));
}
registryBuilder.addDynamicRemoteStrategies(getRemoteStrategies(options));
}
private void throwIfContainsDynamic(List<String> strategies, String flagName)
throws AbruptExitException {
ImmutableSet<String> identifiers = ImmutableSet.of("dynamic", "dynamic_worker");
if (!Sets.intersection(identifiers, ImmutableSet.copyOf(strategies)).isEmpty()) {
String message =
String.format(
"Cannot use strategy %s in flag %s as it would create a cycle during" + " execution",
identifiers, flagName);
throw new AbruptExitException(
DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(message)
.setExecutionOptions(
ExecutionOptions.newBuilder().setCode(Code.INVALID_CYCLIC_DYNAMIC_STRATEGY))
.build()));
}
}
/**
* Use the {@link Spawn} metadata to determine if it can be executed locally, remotely, or both.
*
* @param spawn the {@link Spawn} action
* @return the {@link ExecutionPolicy} containing local/remote execution policies
*/
protected ExecutionPolicy getExecutionPolicy(Spawn spawn) {
if (!Spawns.mayBeExecutedRemotely(spawn)) {
return ExecutionPolicy.LOCAL_EXECUTION_ONLY;
}
if (!Spawns.mayBeExecutedLocally(spawn)) {
return ExecutionPolicy.REMOTE_EXECUTION_ONLY;
}
return ExecutionPolicy.ANYWHERE;
}
/**
* Returns a post processing {@link Spawn} if one needs to be executed after given {@link Spawn}
* when running locally.
*
* <p>The intention of this is to allow post-processing of the original {@linkplain Spawn spawn}
* when executing it locally. In particular, such spawn should never create outputs which are not
* included in the generating action of the original one.
*/
protected Optional<Spawn> getPostProcessingSpawnForLocalExecution(Spawn spawn) {
return Optional.empty();
}
@Subscribe
public void buildCompleteEvent(BuildCompleteEvent event) {
BuildResult result = event.getResult();
if (result.getSuccess()) {
firstBuild = false;
}
}
@Override
public void afterCommand() {
ExecutorUtil.interruptibleShutdown(executorService);
executorService = null;
}
}