// 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.exec; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.HashBasedTable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Iterables; import com.google.common.collect.Ordering; import com.google.common.collect.Table; import com.google.devtools.build.lib.actions.ActionContext; import com.google.devtools.build.lib.actions.ActionContextMarker; import com.google.devtools.build.lib.actions.ActionExecutionContext; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.ExecutionStrategy; import com.google.devtools.build.lib.actions.ExecutorInitException; import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.actions.SpawnResult; import com.google.devtools.build.lib.analysis.test.TestActionContext; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.EventHandler; import com.google.devtools.build.lib.events.Reporter; import com.google.devtools.build.lib.util.ExitCode; import com.google.devtools.build.lib.util.RegexFilter; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; /** * Container for looking up the {@link ActionContext} to use for a given action. * *

Holds {@link ActionContext} mappings populated by modules. These include mappings from * mnemonics and from description patterns. * *

At startup time, the application provides {@link Builder} to each module to register its * contexts and mappings. At runtime, the {@link BlazeExecutor} uses the constructed object to find * the context for each action. */ public final class SpawnActionContextMaps { /** A stored entry for a {@link RegexFilter} to {@link SpawnActionContext} mapping. */ @AutoValue public abstract static class RegexFilterSpawnActionContext { public abstract RegexFilter regexFilter(); public abstract SpawnActionContext spawnActionContext(); } private final ImmutableSortedMap spawnStrategyMnemonicMap; private final ImmutableList strategies; private final ImmutableList spawnStrategyRegexList; private SpawnActionContextMaps( ImmutableSortedMap spawnStrategyMnemonicMap, ImmutableList strategies, ImmutableList spawnStrategyRegexList) { this.spawnStrategyMnemonicMap = spawnStrategyMnemonicMap; this.strategies = strategies; this.spawnStrategyRegexList = spawnStrategyRegexList; } /** * Returns the appropriate {@link ActionContext} to execute the given {@link Spawn} with. * *

If the reason for selecting the context is worth mentioning to the user, logs a message * using the given {@link Reporter}. */ public SpawnActionContext getSpawnActionContext(Spawn spawn, EventHandler reporter) { Preconditions.checkNotNull(spawn); if (!spawnStrategyRegexList.isEmpty() && spawn.getResourceOwner() != null) { String description = spawn.getResourceOwner().getProgressMessage(); if (description != null) { for (RegexFilterSpawnActionContext entry : spawnStrategyRegexList) { if (entry.regexFilter().isIncluded(description) && entry.spawnActionContext() != null) { reporter.handle( Event.info(description + " with context " + entry.spawnActionContext().toString())); return entry.spawnActionContext(); } } } } SpawnActionContext context = spawnStrategyMnemonicMap.get(spawn.getMnemonic()); if (context != null) { return context; } return spawnStrategyMnemonicMap.get(""); } /** Returns a map from action context class to its instantiated context object. */ public ImmutableMap, ActionContext> contextMap() { Map, ActionContext> contextMap = new HashMap<>(); for (ActionContext context : strategies) { ExecutionStrategy annotation = context.getClass().getAnnotation(ExecutionStrategy.class); if (annotation != null) { contextMap.put(annotation.contextType(), context); } contextMap.put(context.getClass(), context); } contextMap.put(SpawnActionContext.class, new ProxySpawnActionContext()); return ImmutableMap.copyOf(contextMap); } /** Returns a list of all referenced {@link ActionContext} instances. */ public ImmutableList allContexts() { // We need to keep only the last occurrences of the entries in contextImplementations // (so we respect insertion order but also instantiate them only once). LinkedHashSet allContexts = new LinkedHashSet<>(); allContexts.addAll(strategies); allContexts.addAll(spawnStrategyMnemonicMap.values()); spawnStrategyRegexList.forEach(x -> allContexts.add(x.spawnActionContext())); return ImmutableList.copyOf(allContexts); } /** * Print a sorted list of our (Spawn)ActionContext maps. * *

Prints out debug information about the mappings. */ public void debugPrintSpawnActionContextMaps(Reporter reporter) { for (Map.Entry entry : spawnStrategyMnemonicMap.entrySet()) { reporter.handle( Event.info( String.format( "SpawnActionContextMap: \"%s\" = %s", entry.getKey(), entry.getValue().getClass().getSimpleName()))); } ImmutableMap, ActionContext> contextMap = contextMap(); TreeMap sortedContextMapWithSimpleNames = new TreeMap<>(); for (Map.Entry, ActionContext> entry : contextMap.entrySet()) { sortedContextMapWithSimpleNames.put( entry.getKey().getSimpleName(), entry.getValue().getClass().getSimpleName()); } for (Map.Entry entry : sortedContextMapWithSimpleNames.entrySet()) { // Skip uninteresting identity mappings of contexts. if (!entry.getKey().equals(entry.getValue())) { reporter.handle( Event.info(String.format("ContextMap: %s = %s", entry.getKey(), entry.getValue()))); } } for (RegexFilterSpawnActionContext entry : spawnStrategyRegexList) { reporter.handle( Event.info( String.format( "SpawnActionContextMap: \"%s\" = %s", entry.regexFilter().toString(), entry.spawnActionContext().getClass().getSimpleName()))); } } @VisibleForTesting public static SpawnActionContextMaps createStub( List strategies, Map spawnStrategyMnemonicMap) { return new SpawnActionContextMaps( ImmutableSortedMap.orderedBy(String.CASE_INSENSITIVE_ORDER) .putAll(spawnStrategyMnemonicMap) .build(), ImmutableList.copyOf(strategies), ImmutableList.of()); } /** A stored entry for a {@link RegexFilter} to {@code strategy} mapping. */ @AutoValue public abstract static class RegexFilterStrategy { public abstract RegexFilter regexFilter(); public abstract String strategy(); } /** Builder for {@code SpawnActionContextMaps}. */ public static final class Builder { private ImmutableListMultimap.Builder strategyByMnemonicMapBuilder = ImmutableListMultimap.builder(); private ImmutableListMultimap.Builder, String> strategyByContextMapBuilder = ImmutableListMultimap.builder(); private final ImmutableList.Builder strategyByRegexpBuilder = ImmutableList.builder(); /** * Returns a builder modules can use to add mappings from mnemonics to strategy names. * *

If a spawn action is executed whose mnemonic maps to the empty string or is not present in * the map at all, the choice of the implementation is left to Blaze. * *

Matching on mnemonics is done case-insensitively so it is recommended that any * module makes sure that no two strategies refer to the same mnemonic. If they do, Blaze * will pick the last one added. */ public ImmutableMultimap.Builder strategyByMnemonicMap() { return strategyByMnemonicMapBuilder; } /** * Returns a builder modules can use to associate {@link ActionContext} classes with * strategy names. */ public ImmutableMultimap.Builder, String> strategyByContextMap() { return strategyByContextMapBuilder; } /** Adds a mapping from the given {@link RegexFilter} to a {@code strategy}. */ public void addStrategyByRegexp(RegexFilter regexFilter, String strategy) { strategyByRegexpBuilder.add( new AutoValue_SpawnActionContextMaps_RegexFilterStrategy(regexFilter, strategy)); } /** Builds a {@link SpawnActionContextMaps} instance. */ public SpawnActionContextMaps build( ImmutableList actionContextProviders, String testStrategyValue) throws ExecutorInitException { StrategyConverter strategyConverter = new StrategyConverter(actionContextProviders); ImmutableSortedMap.Builder spawnStrategyMap = ImmutableSortedMap.orderedBy(String.CASE_INSENSITIVE_ORDER); ImmutableList.Builder strategies = ImmutableList.builder(); ImmutableList.Builder spawnStrategyRegexList = ImmutableList.builder(); ImmutableListMultimap multimap = strategyByMnemonicMapBuilder.build(); for (String mnemonic : multimap.keySet()) { String strategy = Iterables.getLast(multimap.get(mnemonic)); SpawnActionContext context = strategyConverter.getStrategy(SpawnActionContext.class, strategy); if (context == null) { String strategyOrNull = Strings.emptyToNull(strategy); throw makeExceptionForInvalidStrategyValue( strategy, Joiner.on(' ').skipNulls().join(strategyOrNull, "spawn"), strategyConverter.getValidValues(SpawnActionContext.class)); } spawnStrategyMap.put(mnemonic, context); } Set seenContext = new HashSet<>(); for (Map.Entry, String> entry : strategyByContextMapBuilder.orderValuesBy(Collections.reverseOrder()).build().entries()) { ActionContext context = strategyConverter.getStrategy(entry.getKey(), entry.getValue()); if (context == null) { throw makeExceptionForInvalidStrategyValue( entry.getValue(), strategyConverter.getUserFriendlyName(entry.getKey()), strategyConverter.getValidValues(entry.getKey())); } if (seenContext.contains(context)) { continue; } seenContext.add(context); strategies.add(context); } for (RegexFilterStrategy entry : strategyByRegexpBuilder.build()) { SpawnActionContext context = strategyConverter.getStrategy(SpawnActionContext.class, entry.strategy()); if (context == null) { String strategy = Strings.emptyToNull(entry.strategy().toString()); throw makeExceptionForInvalidStrategyValue( entry.regexFilter().toString(), Joiner.on(' ').skipNulls().join(strategy, "spawn"), strategyConverter.getValidValues(SpawnActionContext.class)); } spawnStrategyRegexList.add( new AutoValue_SpawnActionContextMaps_RegexFilterSpawnActionContext( entry.regexFilter(), context)); } ActionContext context = strategyConverter.getStrategy(TestActionContext.class, testStrategyValue); if (context == null) { throw makeExceptionForInvalidStrategyValue( testStrategyValue, "test", strategyConverter.getValidValues(TestActionContext.class)); } strategies.add(context); return new SpawnActionContextMaps( spawnStrategyMap.build(), strategies.build(), spawnStrategyRegexList.build()); } } private static ExecutorInitException makeExceptionForInvalidStrategyValue( String value, String strategy, String validValues) { return new ExecutorInitException( String.format( "'%s' is an invalid value for %s strategy. Valid values are: %s", value, strategy, validValues), ExitCode.COMMAND_LINE_ERROR); } private static class StrategyConverter { private Table, String, ActionContext> classMap = HashBasedTable.create(); private Map, ActionContext> defaultClassMap = new HashMap<>(); /** Aggregates all {@link ActionContext}s that are in {@code contextProviders}. */ @SuppressWarnings("unchecked") private StrategyConverter(Iterable contextProviders) { for (ActionContextProvider provider : contextProviders) { for (ActionContext strategy : provider.getActionContexts()) { ExecutionStrategy annotation = strategy.getClass().getAnnotation(ExecutionStrategy.class); // TODO(ulfjack): Don't silently ignore action contexts without annotation. if (annotation != null) { defaultClassMap.put(annotation.contextType(), strategy); for (String name : annotation.name()) { classMap.put(annotation.contextType(), name, strategy); } } } } } @SuppressWarnings("unchecked") private T getStrategy(Class clazz, String name) { return (T) (name.isEmpty() ? defaultClassMap.get(clazz) : classMap.get(clazz, name)); } private String getValidValues(Class context) { return Joiner.on(", ").join(Ordering.natural().sortedCopy(classMap.row(context).keySet())); } private String getUserFriendlyName(Class context) { ActionContextMarker marker = context.getAnnotation(ActionContextMarker.class); return marker != null ? marker.name() : context.getSimpleName(); } } /** Proxy that looks up the right SpawnActionContext for a spawn during exec. */ @VisibleForTesting public final class ProxySpawnActionContext implements SpawnActionContext { @Override public List exec(Spawn spawn, ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException { return resolve(spawn, actionExecutionContext.getEventHandler()) .exec(spawn, actionExecutionContext); } @VisibleForTesting public SpawnActionContext resolve(Spawn spawn, EventHandler eventHandler) { return getSpawnActionContext(spawn, eventHandler); } } }