| // 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 static com.google.common.base.Preconditions.checkArgument; | |
| import static com.google.common.base.Preconditions.checkState; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static junit.framework.TestCase.fail; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.auto.value.AutoValue; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.Maps; | |
| import com.google.common.flogger.GoogleLogger; | |
| import com.google.common.util.concurrent.Uninterruptibles; | |
| import com.google.devtools.build.lib.actions.ActionContext; | |
| import com.google.devtools.build.lib.actions.ActionExecutionContext; | |
| import com.google.devtools.build.lib.actions.ActionExecutionMetadata; | |
| import com.google.devtools.build.lib.actions.ActionInput; | |
| import com.google.devtools.build.lib.actions.ActionKeyContext; | |
| import com.google.devtools.build.lib.actions.Artifact; | |
| import com.google.devtools.build.lib.actions.ArtifactRoot; | |
| import com.google.devtools.build.lib.actions.BaseSpawn; | |
| import com.google.devtools.build.lib.actions.DynamicStrategyRegistry; | |
| import com.google.devtools.build.lib.actions.EmptyRunfilesSupplier; | |
| import com.google.devtools.build.lib.actions.ExecException; | |
| import com.google.devtools.build.lib.actions.Executor; | |
| import com.google.devtools.build.lib.actions.ExecutorInitException; | |
| import com.google.devtools.build.lib.actions.ResourceSet; | |
| import com.google.devtools.build.lib.actions.SandboxedSpawnStrategy; | |
| import com.google.devtools.build.lib.actions.Spawn; | |
| import com.google.devtools.build.lib.actions.SpawnResult; | |
| import com.google.devtools.build.lib.actions.SpawnStrategy; | |
| import com.google.devtools.build.lib.actions.UserExecException; | |
| import com.google.devtools.build.lib.actions.util.ActionsTestUtil; | |
| import com.google.devtools.build.lib.actions.util.ActionsTestUtil.NullAction; | |
| import com.google.devtools.build.lib.exec.BlazeExecutor; | |
| import com.google.devtools.build.lib.exec.ExecutionOptions; | |
| import com.google.devtools.build.lib.exec.ExecutorBuilder; | |
| import com.google.devtools.build.lib.exec.ModuleActionContextRegistry; | |
| import com.google.devtools.build.lib.exec.SpawnActionContextMaps; | |
| import com.google.devtools.build.lib.exec.SpawnStrategyRegistry; | |
| import com.google.devtools.build.lib.testutil.TestThread; | |
| import com.google.devtools.build.lib.testutil.TestUtils; | |
| import com.google.devtools.build.lib.util.io.FileOutErr; | |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.Root; | |
| import com.google.devtools.build.lib.vfs.util.FileSystems; | |
| import com.google.devtools.common.options.OptionsParser; | |
| import java.io.IOException; | |
| import java.util.Arrays; | |
| import java.util.Collection; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Optional; | |
| import java.util.concurrent.CountDownLatch; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import javax.annotation.Nullable; | |
| import org.junit.After; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.Parameterized; | |
| import org.junit.runners.Parameterized.Parameter; | |
| import org.junit.runners.Parameterized.Parameters; | |
| /** Tests for {@link DynamicSpawnStrategy}. */ | |
| @RunWith(Parameterized.class) | |
| public class DynamicSpawnStrategyTest { | |
| private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); | |
| private Path testRoot; | |
| private ExecutorService executorServiceForCleanup; | |
| private FileOutErr outErr; | |
| private final ActionKeyContext actionKeyContext = new ActionKeyContext(); | |
| @Parameters(name = "{index}: legacy={0}") | |
| public static Collection<Object[]> data() { | |
| return Arrays.asList( | |
| new Object[][] { | |
| {true}, {false}, | |
| }); | |
| } | |
| @Parameter public boolean legacyBehavior; | |
| /** Syntactic sugar to decrease and await for a latch in a single line. */ | |
| private static void countDownAndWait(CountDownLatch countDownLatch) throws InterruptedException { | |
| countDownLatch.countDown(); | |
| countDownLatch.await(); | |
| } | |
| /** Hook to implement per-test custom logic in the {@link MockSpawnStrategy}. */ | |
| @FunctionalInterface | |
| interface DoExec { | |
| DoExec NOTHING = (self, spawn, actionExecutionContext) -> {}; | |
| void run(MockSpawnStrategy self, Spawn spawn, ActionExecutionContext actionExecutionContext) | |
| throws ExecException, InterruptedException; | |
| } | |
| /** | |
| * Minimal implementation of a strategy for testing purposes. | |
| * | |
| * <p>All the logic in here must be applicable to all tests. If any test needs to special-case | |
| * some aspect of this logic, then it must extend this subclass as necessary. | |
| */ | |
| private class MockSpawnStrategy implements SandboxedSpawnStrategy { | |
| /** Identifier of this class for error reporting purposes. */ | |
| private final String name; | |
| /** Lazily set to the spawn passed to {@link #exec} as soon as that hook is invoked. */ | |
| @Nullable private volatile Spawn executedSpawn; | |
| /** Tracks whether {@link #exec} completed successfully or not. */ | |
| private CountDownLatch succeeded = new CountDownLatch(1); | |
| /** Hook to implement per-test custom logic. */ | |
| private final DoExec doExecBeforeStop; | |
| private final DoExec doExecAfterStop; | |
| MockSpawnStrategy(String name) { | |
| this(name, DoExec.NOTHING, DoExec.NOTHING); | |
| } | |
| MockSpawnStrategy(String name, DoExec doExecBeforeStop, DoExec doExecAfterStop) { | |
| this.name = name; | |
| this.doExecBeforeStop = doExecBeforeStop; | |
| this.doExecAfterStop = doExecAfterStop; | |
| } | |
| /** Helper to record an execution failure from within {@link #doExecBeforeStop}. */ | |
| void failExecution(ActionExecutionContext actionExecutionContext) throws ExecException { | |
| try { | |
| FileSystemUtils.appendIsoLatin1( | |
| actionExecutionContext.getFileOutErr().getOutputPath(), "action failed with " + name); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| throw new UserExecException(name + " failed to execute the Spawn"); | |
| } | |
| @Override | |
| public ImmutableList<SpawnResult> exec( | |
| Spawn spawn, | |
| ActionExecutionContext actionExecutionContext, | |
| @Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns) | |
| throws ExecException, InterruptedException { | |
| executedSpawn = spawn; | |
| doExecBeforeStop.run(this, spawn, actionExecutionContext); | |
| if (stopConcurrentSpawns != null) { | |
| stopConcurrentSpawns.stop(); | |
| doExecAfterStop.run(this, spawn, actionExecutionContext); | |
| } | |
| for (ActionInput output : spawn.getOutputFiles()) { | |
| try { | |
| FileSystemUtils.writeIsoLatin1(testRoot.getRelative(output.getExecPath()), name); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| try { | |
| FileSystemUtils.appendIsoLatin1( | |
| actionExecutionContext.getFileOutErr().getOutputPath(), | |
| "output files written with " + name); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| succeeded.countDown(); | |
| return ImmutableList.of(); | |
| } | |
| @Override | |
| public ImmutableList<SpawnResult> exec( | |
| Spawn spawn, ActionExecutionContext actionExecutionContext) { | |
| throw new IllegalStateException("Not expected to be called"); | |
| } | |
| @Override | |
| public boolean canExec(Spawn spawn, ActionContext.ActionContextRegistry actionContextRegistry) { | |
| return true; | |
| } | |
| @Nullable | |
| public Spawn getExecutedSpawn() { | |
| return executedSpawn; | |
| } | |
| /** Returns true if {@link #exec} was called and completed successfully; does not block. */ | |
| boolean succeeded() { | |
| return succeeded.getCount() == 0; | |
| } | |
| } | |
| @Before | |
| public void setUp() throws Exception { | |
| testRoot = FileSystems.getNativeFileSystem().getPath(TestUtils.tmpDir()); | |
| testRoot.deleteTreesBelow(); | |
| outErr = new FileOutErr(testRoot.getRelative("stdout"), testRoot.getRelative("stderr")); | |
| } | |
| /** | |
| * Creates a new dynamic spawn strategy with different strategies for local and remote execution | |
| * and a default multi-threaded executor service. | |
| * | |
| * @param localStrategy the strategy for local execution | |
| * @param remoteStrategy the strategy for remote execution | |
| * @return the constructed dynamic strategy | |
| * @throws ExecutorInitException if creating the strategy with the given parameters fails | |
| */ | |
| private StrategyAndContext createSpawnStrategy( | |
| MockSpawnStrategy localStrategy, MockSpawnStrategy remoteStrategy) | |
| throws ExecutorInitException { | |
| return createSpawnStrategyWithExecutor( | |
| localStrategy, remoteStrategy, Executors.newCachedThreadPool()); | |
| } | |
| /** | |
| * Creates a new dynamic spawn strategy with different strategies for local, remote, and sandboxed | |
| * execution. | |
| * | |
| * <p>TODO(jmmv): This overload should not be necessary now that we do not special-case the | |
| * handling of sandboxed strategies any longer. Remove once the sandbox-specific flags are gone. | |
| * | |
| * @param localStrategy the default strategy for local execution | |
| * @param remoteStrategy the default strategy for remote execution | |
| * @param sandboxedStrategy the strategy to use when the mnemonic matches {@code testMnemonic}. | |
| * @return the constructed dynamic strategy | |
| * @throws ExecutorInitException if creating the strategy with the given parameters fails | |
| */ | |
| private StrategyAndContext createSpawnStrategy( | |
| MockSpawnStrategy localStrategy, | |
| MockSpawnStrategy remoteStrategy, | |
| @Nullable MockSpawnStrategy sandboxedStrategy) | |
| throws ExecutorInitException { | |
| return createSpawnStrategyWithExecutor( | |
| localStrategy, remoteStrategy, sandboxedStrategy, Executors.newCachedThreadPool()); | |
| } | |
| /** | |
| * Creates a new dynamic spawn strategy with different strategies for local and remote execution. | |
| * | |
| * @param localStrategy the strategy for local execution | |
| * @param remoteStrategy the strategy for remote execution | |
| * @param executorService the executor to pass to the dynamic strategy | |
| * @return the constructed dynamic strategy | |
| * @throws ExecutorInitException if creating the strategy with the given parameters fails | |
| */ | |
| private StrategyAndContext createSpawnStrategyWithExecutor( | |
| MockSpawnStrategy localStrategy, | |
| MockSpawnStrategy remoteStrategy, | |
| ExecutorService executorService) | |
| throws ExecutorInitException { | |
| return createSpawnStrategyWithExecutor(localStrategy, remoteStrategy, null, executorService); | |
| } | |
| /** | |
| * Creates a new dynamic spawn strategy with different strategies for local, remote, and sandboxed | |
| * execution. | |
| * | |
| * <p>TODO(jmmv): This overload should not be necessary now that we do not special-case the | |
| * handling of sandboxed strategies any longer. Remove once the sandbox-specific flags are gone. | |
| * | |
| * @param localStrategy the default strategy for local execution | |
| * @param remoteStrategy the default strategy for remote execution | |
| * @param sandboxedStrategy the strategy to use when the mnemonic matches {@code testMnemonic}. | |
| * @param executorService the executor to pass to the dynamic strategy | |
| * @return the constructed dynamic strategy | |
| * @throws ExecutorInitException if creating the strategy with the given parameters fails | |
| */ | |
| private StrategyAndContext createSpawnStrategyWithExecutor( | |
| MockSpawnStrategy localStrategy, | |
| MockSpawnStrategy remoteStrategy, | |
| @Nullable MockSpawnStrategy sandboxedStrategy, | |
| ExecutorService executorService) | |
| throws ExecutorInitException { | |
| ImmutableList.Builder<Map.Entry<String, List<String>>> dynamicLocalStrategies = | |
| ImmutableList.<Map.Entry<String, List<String>>>builder() | |
| .add(Maps.immutableEntry("", ImmutableList.of("mock-local"))); | |
| ImmutableList.Builder<Map.Entry<String, List<String>>> dynamicRemoteStrategies = | |
| ImmutableList.<Map.Entry<String, List<String>>>builder() | |
| .add(Maps.immutableEntry("", ImmutableList.of("mock-remote"))); | |
| if (sandboxedStrategy != null) { | |
| dynamicLocalStrategies.add( | |
| Maps.immutableEntry("testMnemonic", ImmutableList.of("mock-sandboxed"))); | |
| dynamicRemoteStrategies.add( | |
| Maps.immutableEntry("testMnemonic", ImmutableList.of("mock-sandboxed"))); | |
| } | |
| DynamicExecutionOptions options = new DynamicExecutionOptions(); | |
| options.dynamicLocalStrategy = dynamicLocalStrategies.build(); | |
| options.dynamicRemoteStrategy = dynamicRemoteStrategies.build(); | |
| options.dynamicWorkerStrategy = "mock-local"; | |
| options.internalSpawnScheduler = true; | |
| options.localExecutionDelay = 0; | |
| options.legacySpawnScheduler = legacyBehavior; | |
| checkState(executorServiceForCleanup == null); | |
| executorServiceForCleanup = executorService; | |
| ExecutorBuilder executorBuilder = new ExecutorBuilder(); | |
| ModuleActionContextRegistry.Builder moduleActionContextRegistryBuilder = | |
| executorBuilder.asModuleActionContextRegistryBuilder(ModuleActionContextRegistry.builder()); | |
| SpawnStrategyRegistry.Builder spawnStrategyRegistryBuilder = | |
| executorBuilder.asSpawnStrategyRegistryBuilder(SpawnStrategyRegistry.builder()); | |
| spawnStrategyRegistryBuilder.registerStrategy(localStrategy, "mock-local"); | |
| spawnStrategyRegistryBuilder.registerStrategy(remoteStrategy, "mock-remote"); | |
| if (sandboxedStrategy != null) { | |
| spawnStrategyRegistryBuilder.registerStrategy(sandboxedStrategy, "mock-sandboxed"); | |
| } | |
| DynamicExecutionModule dynamicExecutionModule = new DynamicExecutionModule(executorService); | |
| // TODO(b/63987502): This only exists during the migration to SpawnStrategyRegistry. | |
| executorBuilder.addStrategyByContext(SpawnStrategy.class, "dynamic"); | |
| dynamicExecutionModule.registerSpawnStrategies(spawnStrategyRegistryBuilder, options); | |
| SpawnStrategyRegistry spawnStrategyRegistry = spawnStrategyRegistryBuilder.build(); | |
| executorBuilder.addActionContext(SpawnStrategyRegistry.class, spawnStrategyRegistry); | |
| executorBuilder.addStrategyByContext(SpawnStrategyRegistry.class, ""); | |
| moduleActionContextRegistryBuilder.register( | |
| DynamicStrategyRegistry.class, spawnStrategyRegistry); | |
| ModuleActionContextRegistry moduleActionContextRegistry = | |
| moduleActionContextRegistryBuilder.build(); | |
| executorBuilder.addActionContext( | |
| ModuleActionContextRegistry.class, moduleActionContextRegistry); | |
| executorBuilder.addStrategyByContext(ModuleActionContextRegistry.class, ""); | |
| SpawnActionContextMaps spawnActionContextMaps = executorBuilder.getSpawnActionContextMaps(); | |
| Executor executor = | |
| new BlazeExecutor( | |
| null, | |
| testRoot, | |
| null, | |
| null, | |
| OptionsParser.builder() | |
| .optionsClasses(ImmutableList.of(ExecutionOptions.class)) | |
| .build(), | |
| spawnActionContextMaps); | |
| ActionExecutionContext actionExecutionContext = | |
| ActionsTestUtil.createContext( | |
| /*executor=*/ executor, | |
| /*eventHandler=*/ null, | |
| actionKeyContext, | |
| outErr, | |
| testRoot, | |
| /*metadataHandler=*/ null, | |
| /*actionGraph=*/ null); | |
| Optional<ActionContext> optionalContext = | |
| spawnActionContextMaps.allContexts().stream() | |
| .filter( | |
| c -> c instanceof DynamicSpawnStrategy || c instanceof LegacyDynamicSpawnStrategy) | |
| .findAny(); | |
| checkState(optionalContext.isPresent(), "Expected module to register a dynamic strategy"); | |
| return new AutoValue_DynamicSpawnStrategyTest_StrategyAndContext( | |
| (SpawnStrategy) optionalContext.get(), actionExecutionContext); | |
| } | |
| private static class NullActionWithMnemonic extends NullAction { | |
| private final String mnemonic; | |
| private NullActionWithMnemonic(String mnemonic, List<Artifact> inputs, Artifact... outputs) { | |
| super(inputs, outputs); | |
| this.mnemonic = mnemonic; | |
| } | |
| @Override | |
| public String getMnemonic() { | |
| return mnemonic; | |
| } | |
| } | |
| @After | |
| public void tearDown() throws Exception { | |
| if (executorServiceForCleanup != null) { | |
| executorServiceForCleanup.shutdownNow(); | |
| } | |
| } | |
| /** Constructs a new spawn with a custom mnemonic and execution info. */ | |
| private Spawn newCustomSpawn(String mnemonic, ImmutableMap<String, String> executionInfo) { | |
| Artifact inputArtifact = | |
| ActionsTestUtil.createArtifact( | |
| ArtifactRoot.asSourceRoot(Root.fromPath(testRoot)), "input.txt"); | |
| Artifact outputArtifact = | |
| ActionsTestUtil.createArtifact( | |
| ArtifactRoot.asSourceRoot(Root.fromPath(testRoot)), "output.txt"); | |
| ActionExecutionMetadata action = | |
| new NullActionWithMnemonic(mnemonic, ImmutableList.of(inputArtifact), outputArtifact); | |
| return new BaseSpawn( | |
| ImmutableList.of(), | |
| ImmutableMap.of(), | |
| executionInfo, | |
| EmptyRunfilesSupplier.INSTANCE, | |
| action, | |
| ResourceSet.create(1, 0, 0)); | |
| } | |
| /** Constructs a new spawn that can be run locally and remotely with arbitrary settings. */ | |
| private Spawn newDynamicSpawn() { | |
| return newCustomSpawn("Null", ImmutableMap.of()); | |
| } | |
| @Test | |
| public void nonRemotableSpawnRunsLocally() throws Exception { | |
| MockSpawnStrategy localStrategy = new MockSpawnStrategy("MockLocalSpawnStrategy"); | |
| MockSpawnStrategy remoteStrategy = new MockSpawnStrategy("MockRemoteSpawnStrategy"); | |
| StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy); | |
| Spawn spawn = newCustomSpawn("Null", ImmutableMap.of("local", "1")); | |
| strategyAndContext.exec(spawn); | |
| assertThat(localStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(localStrategy.succeeded()).isTrue(); | |
| assertThat(remoteStrategy.getExecutedSpawn()).isNull(); | |
| assertThat(remoteStrategy.succeeded()).isFalse(); | |
| assertThat(outErr.outAsLatin1()).contains("output files written with MockLocalSpawnStrategy"); | |
| assertThat(outErr.outAsLatin1()).doesNotContain("MockRemoteSpawnStrategy"); | |
| } | |
| @Test | |
| public void localSpawnUsesStrategyByMnemonicWithWorkerFlagDisabled() throws Exception { | |
| MockSpawnStrategy localStrategy = new MockSpawnStrategy("MockLocalSpawnStrategy"); | |
| MockSpawnStrategy remoteStrategy = new MockSpawnStrategy("MockRemoteSpawnStrategy"); | |
| MockSpawnStrategy sandboxedStrategy = new MockSpawnStrategy("MockSandboxedSpawnStrategy"); | |
| StrategyAndContext strategyAndContext = | |
| createSpawnStrategy(localStrategy, remoteStrategy, sandboxedStrategy); | |
| Spawn spawn = newCustomSpawn("testMnemonic", ImmutableMap.of("local", "1")); | |
| strategyAndContext.exec(spawn); | |
| assertThat(localStrategy.getExecutedSpawn()).isNull(); | |
| assertThat(localStrategy.succeeded()).isFalse(); | |
| assertThat(remoteStrategy.getExecutedSpawn()).isNull(); | |
| assertThat(remoteStrategy.succeeded()).isFalse(); | |
| assertThat(sandboxedStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(sandboxedStrategy.succeeded()).isTrue(); | |
| assertThat(outErr.outAsLatin1()) | |
| .contains("output files written with MockSandboxedSpawnStrategy"); | |
| assertThat(outErr.outAsLatin1()).doesNotContain("MockLocalSpawnStrategy"); | |
| assertThat(outErr.outAsLatin1()).doesNotContain("MockRemoteSpawnStrategy"); | |
| } | |
| @Test | |
| public void remoteSpawnUsesStrategyByMnemonic() throws Exception { | |
| MockSpawnStrategy localStrategy = new MockSpawnStrategy("MockLocalSpawnStrategy"); | |
| MockSpawnStrategy remoteStrategy = new MockSpawnStrategy("MockRemoteSpawnStrategy"); | |
| MockSpawnStrategy sandboxedStrategy = new MockSpawnStrategy("MockSandboxedSpawnStrategy"); | |
| StrategyAndContext strategyAndContext = | |
| createSpawnStrategy(localStrategy, remoteStrategy, sandboxedStrategy); | |
| Spawn spawn = newCustomSpawn("testMnemonic", ImmutableMap.of("remote", "1")); | |
| strategyAndContext.exec(spawn); | |
| assertThat(localStrategy.getExecutedSpawn()).isNull(); | |
| assertThat(localStrategy.succeeded()).isFalse(); | |
| assertThat(remoteStrategy.getExecutedSpawn()).isNull(); | |
| assertThat(remoteStrategy.succeeded()).isFalse(); | |
| assertThat(sandboxedStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(sandboxedStrategy.succeeded()).isTrue(); | |
| assertThat(outErr.outAsLatin1()) | |
| .contains("output files written with MockSandboxedSpawnStrategy"); | |
| assertThat(outErr.outAsLatin1()).doesNotContain("MockLocalSpawnStrategy"); | |
| assertThat(outErr.outAsLatin1()).doesNotContain("MockRemoteSpawnStrategy"); | |
| } | |
| @Test | |
| public void actionSucceedsIfLocalExecutionSucceedsEvenIfRemoteFailsLater() throws Exception { | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| MockSpawnStrategy localStrategy = | |
| new MockSpawnStrategy( | |
| "MockLocalSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> countDownAndWait(countDownLatch), | |
| DoExec.NOTHING); | |
| MockSpawnStrategy remoteStrategy = | |
| new MockSpawnStrategy( | |
| "MockRemoteSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| countDownAndWait(countDownLatch); | |
| Thread.sleep(2000); | |
| self.failExecution(actionExecutionContext); | |
| }, | |
| DoExec.NOTHING); | |
| StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy); | |
| Spawn spawn = newDynamicSpawn(); | |
| strategyAndContext.exec(spawn); | |
| assertThat(localStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(localStrategy.succeeded()).isTrue(); | |
| assertThat(remoteStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(remoteStrategy.succeeded()).isFalse(); | |
| assertThat(outErr.outAsLatin1()).contains("output files written with MockLocalSpawnStrategy"); | |
| assertThat(outErr.outAsLatin1()).doesNotContain("MockRemoteSpawnStrategy"); | |
| } | |
| @Test | |
| public void actionSucceedsIfRemoteExecutionSucceedsEvenIfLocalFailsLater() throws Exception { | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| MockSpawnStrategy localStrategy = | |
| new MockSpawnStrategy( | |
| "MockLocalSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| countDownAndWait(countDownLatch); | |
| Thread.sleep(2000); | |
| self.failExecution(actionExecutionContext); | |
| }, | |
| DoExec.NOTHING); | |
| MockSpawnStrategy remoteStrategy = | |
| new MockSpawnStrategy( | |
| "MockRemoteSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> countDownAndWait(countDownLatch), | |
| DoExec.NOTHING); | |
| StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy); | |
| Spawn spawn = newDynamicSpawn(); | |
| strategyAndContext.exec(spawn); | |
| assertThat(localStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(localStrategy.succeeded()).isFalse(); | |
| assertThat(remoteStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(remoteStrategy.succeeded()).isTrue(); | |
| assertThat(outErr.outAsLatin1()).contains("output files written with MockRemoteSpawnStrategy"); | |
| assertThat(outErr.outAsLatin1()).doesNotContain("MockLocalSpawnStrategy"); | |
| } | |
| @Test | |
| public void actionFailsIfLocalFailsImmediatelyEvenIfRemoteSucceedsLater() throws Exception { | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| MockSpawnStrategy localStrategy = | |
| new MockSpawnStrategy( | |
| "MockLocalSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| countDownAndWait(countDownLatch); | |
| self.failExecution(actionExecutionContext); | |
| }, | |
| DoExec.NOTHING); | |
| MockSpawnStrategy remoteStrategy = | |
| new MockSpawnStrategy( | |
| "MockRemoteSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| countDownAndWait(countDownLatch); | |
| Thread.sleep(2000); | |
| }, | |
| DoExec.NOTHING); | |
| StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy); | |
| Spawn spawn = newDynamicSpawn(); | |
| ExecException e = assertThrows(ExecException.class, () -> strategyAndContext.exec(spawn)); | |
| assertThat(e).hasMessageThat().matches("MockLocalSpawnStrategy failed to execute the Spawn"); | |
| assertThat(localStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(localStrategy.succeeded()).isFalse(); | |
| assertThat(remoteStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(remoteStrategy.succeeded()).isFalse(); | |
| assertThat(outErr.outAsLatin1()).contains("action failed with MockLocalSpawnStrategy"); | |
| assertThat(outErr.outAsLatin1()).doesNotContain("MockRemoteSpawnStrategy"); | |
| } | |
| @Test | |
| public void actionFailsIfRemoteFailsImmediatelyEvenIfLocalSucceedsLater() throws Exception { | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| MockSpawnStrategy localStrategy = | |
| new MockSpawnStrategy( | |
| "MockLocalSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| countDownAndWait(countDownLatch); | |
| Thread.sleep(2000); | |
| }, | |
| DoExec.NOTHING); | |
| MockSpawnStrategy remoteStrategy = | |
| new MockSpawnStrategy( | |
| "MockRemoteSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| countDownAndWait(countDownLatch); | |
| self.failExecution(actionExecutionContext); | |
| }, | |
| DoExec.NOTHING); | |
| StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy); | |
| Spawn spawn = newDynamicSpawn(); | |
| ExecException e = assertThrows(ExecException.class, () -> strategyAndContext.exec(spawn)); | |
| assertThat(e).hasMessageThat().matches("MockRemoteSpawnStrategy failed to execute the Spawn"); | |
| assertThat(localStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(localStrategy.succeeded()).isFalse(); | |
| assertThat(remoteStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(remoteStrategy.succeeded()).isFalse(); | |
| assertThat(outErr.outAsLatin1()).contains("action failed with MockRemoteSpawnStrategy"); | |
| assertThat(outErr.outAsLatin1()).doesNotContain("MockLocalSpawnStrategy"); | |
| } | |
| @Test | |
| public void actionFailsIfLocalAndRemoteFail() throws Exception { | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| MockSpawnStrategy localStrategy = | |
| new MockSpawnStrategy( | |
| "MockLocalSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| countDownAndWait(countDownLatch); | |
| self.failExecution(actionExecutionContext); | |
| }, | |
| DoExec.NOTHING); | |
| MockSpawnStrategy remoteStrategy = | |
| new MockSpawnStrategy( | |
| "MockRemoteSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| countDownAndWait(countDownLatch); | |
| self.failExecution(actionExecutionContext); | |
| }, | |
| DoExec.NOTHING); | |
| StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy); | |
| Spawn spawn = newDynamicSpawn(); | |
| ExecException e = assertThrows(ExecException.class, () -> strategyAndContext.exec(spawn)); | |
| assertThat(e) | |
| .hasMessageThat() | |
| .matches("Mock(Local|Remote)SpawnStrategy failed to execute the Spawn"); | |
| assertThat(localStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(localStrategy.succeeded()).isFalse(); | |
| assertThat(remoteStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(remoteStrategy.succeeded()).isFalse(); | |
| } | |
| @Test | |
| public void stopConcurrentSpawnsWaitForCompletion() throws Exception { | |
| if (legacyBehavior) { | |
| // The legacy spawn scheduler does not implement cross-cancellations of the two parallel | |
| // branches so this test makes no sense in that case. | |
| logger.atInfo().log("Skipping test"); | |
| return; | |
| } | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| AtomicBoolean slowCleanupFinished = new AtomicBoolean(false); | |
| MockSpawnStrategy localStrategy = | |
| new MockSpawnStrategy( | |
| "MockLocalSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| try { | |
| countDownAndWait(countDownLatch); | |
| // Block indefinitely waiting for the remote branch to interrupt us. | |
| Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| fail("Should have been interrupted"); | |
| } catch (InterruptedException e) { | |
| // Wait for "long enough" hoping that the remoteStrategy will have enough time to | |
| // check the value of slowCleanupFinished before we finish this sleep, in case we | |
| // have a bug. | |
| Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS); | |
| slowCleanupFinished.set(true); | |
| } | |
| }, | |
| DoExec.NOTHING); | |
| MockSpawnStrategy remoteStrategy = | |
| new MockSpawnStrategy( | |
| "MockRemoteSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> countDownAndWait(countDownLatch), | |
| (self, spawn, actionExecutionContext) -> { | |
| // This runs after we have asked the local spawn to complete and, in theory, awaited | |
| // for InterruptedException to propagate. Make sure that's the case here by checking | |
| // that we did indeed wait for the slow process. | |
| if (!slowCleanupFinished.get()) { | |
| fail("Did not await for the other branch to do its cleanup"); | |
| } | |
| }); | |
| StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy); | |
| Spawn spawn = newDynamicSpawn(); | |
| strategyAndContext.exec(spawn); | |
| assertThat(localStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(localStrategy.succeeded()).isFalse(); | |
| assertThat(remoteStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(remoteStrategy.succeeded()).isTrue(); | |
| } | |
| @Test | |
| public void noDeadlockWithSingleThreadedExecutor() throws Exception { | |
| MockSpawnStrategy localStrategy = new MockSpawnStrategy("MockLocalSpawnStrategy"); | |
| MockSpawnStrategy remoteStrategy = new MockSpawnStrategy("MockRemoteSpawnStrategy"); | |
| StrategyAndContext strategyAndContext = | |
| createSpawnStrategyWithExecutor( | |
| localStrategy, remoteStrategy, Executors.newSingleThreadExecutor()); | |
| Spawn spawn = newDynamicSpawn(); | |
| strategyAndContext.exec(spawn); | |
| assertThat(localStrategy.getExecutedSpawn()).isEqualTo(spawn); | |
| assertThat(localStrategy.succeeded()).isTrue(); | |
| /* | |
| * The single-threaded executorService#invokeAny does not comply to the contract where | |
| * the callables are *always* called sequentially. In this case, both spawns will start | |
| * executing, but the local one will always succeed as it's the first to be called. The remote | |
| * one will then be cancelled, or is null if the local one completes before the remote one | |
| * starts. | |
| * | |
| * See the documentation of {@link BoundedExectorService#invokeAny(Collection)}, specifically: | |
| * "The following is less efficient (it goes on submitting tasks even if there is some task | |
| * already finished), but quite straight-forward.". | |
| */ | |
| assertThat(remoteStrategy.getExecutedSpawn()).isAnyOf(spawn, null); | |
| assertThat(remoteStrategy.succeeded()).isFalse(); | |
| } | |
| @Test | |
| public void interruptDuringExecutionDoesActuallyInterruptTheExecution() throws Exception { | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| MockSpawnStrategy localStrategy = | |
| new MockSpawnStrategy( | |
| "MockLocalSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| countDownAndWait(countDownLatch); | |
| Thread.sleep(60000); | |
| }, | |
| DoExec.NOTHING); | |
| MockSpawnStrategy remoteStrategy = | |
| new MockSpawnStrategy( | |
| "MockRemoteSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| countDownAndWait(countDownLatch); | |
| Thread.sleep(60000); | |
| }, | |
| DoExec.NOTHING); | |
| StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy); | |
| TestThread testThread = | |
| new TestThread( | |
| () -> { | |
| try { | |
| Spawn spawn = newDynamicSpawn(); | |
| strategyAndContext.exec(spawn); | |
| } catch (InterruptedException e) { | |
| // This is expected. | |
| } | |
| }); | |
| testThread.start(); | |
| countDownLatch.await(5, TimeUnit.SECONDS); | |
| testThread.interrupt(); | |
| testThread.joinAndAssertState(5000); | |
| assertThat(outErr.getOutputPath().exists()).isFalse(); | |
| assertThat(outErr.getErrorPath().exists()).isFalse(); | |
| } | |
| /** Hook to validate the result of the strategy's execution. */ | |
| @FunctionalInterface | |
| interface CheckExecResult { | |
| void check(@Nullable Exception e) throws Exception; | |
| } | |
| /** | |
| * Runs a test to check that both spawns finished under various conditions before the strategy's | |
| * {@code exec} method returns control. | |
| * | |
| * @param executionFails causes one of the branches in the execution to terminate with an | |
| * execution exception | |
| * @param interruptThread causes the strategy's execution to be interrupted while it is waiting | |
| * for its branches to complete | |
| * @param checkExecResult a lambda to validate the result of the execution. Receives null if the | |
| * execution completed successfully, or else the raised exception. | |
| */ | |
| private void assertThatStrategyWaitsForBothSpawnsToFinish( | |
| boolean executionFails, boolean interruptThread, CheckExecResult checkExecResult) | |
| throws Exception { | |
| if (!legacyBehavior) { | |
| // TODO(jmmv): I've spent *days* trying to make these tests work reliably with the new dynamic | |
| // spawn scheduler implementation but I keep encountering tricky race conditions everywhere. I | |
| // have strong reasons to believe that the races are due to inherent problems in these tests, | |
| // not in the actual DynamicSpawnScheduler implementation. So whatever. I'll revisit these | |
| // later as a new set of tests once I'm less tired^W^W^W the legacy spawn scheduler goes away. | |
| logger.atInfo().log("Skipping test"); | |
| return; | |
| } | |
| AtomicBoolean stopLocal = new AtomicBoolean(false); | |
| CountDownLatch executionCanProceed = new CountDownLatch(2); | |
| CountDownLatch remoteDone = new CountDownLatch(1); | |
| MockSpawnStrategy localStrategy = | |
| new MockSpawnStrategy( | |
| "MockLocalSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| executionCanProceed.countDown(); | |
| // We cannot use a synchronization primitive to block termination of this thread | |
| // because we expect to be interrupted by the remote strategy, and even in that case | |
| // we want to control exactly when this finishes. We could wait for and swallow the | |
| // interrupt before waiting again on a latch here... but swallowing the interrupt can | |
| // lead to race conditions. | |
| while (!stopLocal.get()) { | |
| Uninterruptibles.sleepUninterruptibly(1, TimeUnit.MILLISECONDS); | |
| } | |
| throw new InterruptedException("Local stopped"); | |
| }, | |
| DoExec.NOTHING); | |
| MockSpawnStrategy remoteStrategy = | |
| new MockSpawnStrategy( | |
| "MockRemoteSpawnStrategy", | |
| (self, spawn, actionExecutionContext) -> { | |
| try { | |
| // Wait until the local branch has started so that our completion causes it to be | |
| // interrupted in a known location. | |
| countDownAndWait(executionCanProceed); | |
| if (executionFails) { | |
| self.failExecution(actionExecutionContext); | |
| throw new AssertionError("Not reachable"); | |
| } | |
| } finally { | |
| remoteDone.countDown(); | |
| } | |
| }, | |
| DoExec.NOTHING); | |
| StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy); | |
| TestThread testThread = | |
| new TestThread( | |
| () -> { | |
| try { | |
| Spawn spawn = newDynamicSpawn(); | |
| strategyAndContext.exec(spawn); | |
| checkExecResult.check(null); | |
| } catch (Exception e) { | |
| checkExecResult.check(e); | |
| } | |
| }); | |
| testThread.start(); | |
| try { | |
| remoteDone.await(); | |
| // At this point, the remote branch is done and the local branch is waiting until we allow it | |
| // to complete later on. This is necessary to let us assert the state of the thread's | |
| // liveliness. | |
| // | |
| // However, note that "done" just means that our DoExec hook for remoteStrategy finished. | |
| // Any exception raised from within it may still be propagating up, so the interrupt below | |
| // races with that (and thus an InterruptedException can "win" over our own exception). There | |
| // is no way to handle this condition in the test other than having to acknowledge that it may | |
| // happen. | |
| if (interruptThread) { | |
| testThread.interrupt(); | |
| } | |
| // The thread running the exec via the strategy must still be alive regardless of our | |
| // interrupt request (because the local branch is stuck). Wait for a little bit to ensure | |
| // this is true; any multi-second wait should be sufficient to catch the majority of the | |
| // bugs. | |
| testThread.join(2000); | |
| assertThat(testThread.isAlive()).isTrue(); | |
| } finally { | |
| // Unblocking the local branch allows the strategy to collect its result and then unblock the | |
| // thread. | |
| stopLocal.set(true); | |
| testThread.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| } | |
| } | |
| @Test | |
| public void strategyWaitsForBothSpawnsToFinish() throws Exception { | |
| assertThatStrategyWaitsForBothSpawnsToFinish( | |
| /*executionFails=*/ false, | |
| /*interruptThread=*/ false, | |
| (e) -> { | |
| if (e != null) { | |
| throw new IllegalStateException("Expected exec to finish successfully", e); | |
| } | |
| }); | |
| } | |
| @Test | |
| public void strategyWaitsForBothSpawnsToFinishEvenIfInterrupted() throws Exception { | |
| assertThatStrategyWaitsForBothSpawnsToFinish( | |
| /*executionFails=*/ false, | |
| /*interruptThread=*/ true, | |
| (e) -> { | |
| if (e == null) { | |
| fail("No exception raised"); | |
| } else if (e instanceof InterruptedException) { | |
| assertThat(Thread.currentThread().isInterrupted()).isFalse(); | |
| } else { | |
| throw e; | |
| } | |
| }); | |
| } | |
| @Test | |
| public void strategyWaitsForBothSpawnsToFinishOnFailure() throws Exception { | |
| assertThatStrategyWaitsForBothSpawnsToFinish( | |
| /*executionFails=*/ true, | |
| /*interruptThread=*/ false, | |
| (e) -> { | |
| if (e == null) { | |
| fail("No exception raised"); | |
| } else if (e instanceof ExecException) { | |
| assertThat(Thread.currentThread().isInterrupted()).isFalse(); | |
| } else { | |
| throw e; | |
| } | |
| }); | |
| } | |
| @Test | |
| public void strategyWaitsForBothSpawnsToFinishOnFailureEvenIfInterrupted() throws Exception { | |
| assertThatStrategyWaitsForBothSpawnsToFinish( | |
| /*executionFails=*/ true, | |
| /*interruptThread=*/ true, | |
| (e) -> { | |
| if (e == null) { | |
| fail("No exception raised"); | |
| } else if (e instanceof InterruptedException) { | |
| // See comment in strategyWaitsForBothSpawnsToFinish regarding the race between the | |
| // exception we raise on failure and the interrupt. We have to handle this case even | |
| // though it is supposedly rare. | |
| } else if (e instanceof ExecException) { | |
| assertThat(Thread.currentThread().isInterrupted()).isTrue(); | |
| } else { | |
| throw e; | |
| } | |
| }); | |
| } | |
| private void assertThatStrategyPropagatesException( | |
| DoExec localExec, DoExec remoteExec, Exception expectedException) throws Exception { | |
| checkArgument( | |
| !(expectedException instanceof IllegalStateException), | |
| "Using an IllegalStateException for testing is fragile because we use that exception " | |
| + "internally in the DynamicSpawnScheduler and we cannot distinguish it from the " | |
| + "test's own exception"); | |
| MockSpawnStrategy localStrategy = | |
| new MockSpawnStrategy("MockLocalSpawnStrategy", localExec, DoExec.NOTHING); | |
| MockSpawnStrategy remoteStrategy = | |
| new MockSpawnStrategy("MockRemoteSpawnStrategy", remoteExec, DoExec.NOTHING); | |
| StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy); | |
| Spawn spawn = newDynamicSpawn(); | |
| Exception e = assertThrows(expectedException.getClass(), () -> strategyAndContext.exec(spawn)); | |
| assertThat(e).hasMessageThat().matches(expectedException.getMessage()); | |
| Spawn executedSpawn = localStrategy.getExecutedSpawn(); | |
| executedSpawn = executedSpawn == null ? remoteStrategy.getExecutedSpawn() : executedSpawn; | |
| assertThat(executedSpawn).isEqualTo(spawn); | |
| assertThat(localStrategy.succeeded()).isFalse(); | |
| assertThat(remoteStrategy.succeeded()).isFalse(); | |
| } | |
| @Test | |
| public void strategyPropagatesFasterLocalException() throws Exception { | |
| RuntimeException e = new IllegalArgumentException("Local spawn execution exception"); | |
| DoExec localExec = | |
| (self, spawn, actionExecutionContext) -> { | |
| throw e; | |
| }; | |
| DoExec remoteExec = | |
| (self, spawn, actionExecutionContext) -> { | |
| Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| throw new AssertionError("Not reachable"); | |
| }; | |
| assertThatStrategyPropagatesException( | |
| localExec, remoteExec, legacyBehavior ? new UserExecException(e) : e); | |
| } | |
| @Test | |
| public void strategyPropagatesFasterRemoteException() throws Exception { | |
| DoExec localExec = | |
| (self, spawn, actionExecutionContext) -> { | |
| Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| throw new AssertionError("Not reachable"); | |
| }; | |
| RuntimeException e = new IllegalArgumentException("Remote spawn execution exception"); | |
| DoExec remoteExec = | |
| (self, spawn, actionExecutionContext) -> { | |
| throw e; | |
| }; | |
| assertThatStrategyPropagatesException( | |
| localExec, remoteExec, legacyBehavior ? new UserExecException(e) : e); | |
| } | |
| @AutoValue | |
| abstract static class StrategyAndContext { | |
| abstract SpawnStrategy strategy(); | |
| abstract ActionExecutionContext context(); | |
| void exec(Spawn spawn) throws ExecException, InterruptedException { | |
| strategy().exec(spawn, context()); | |
| } | |
| } | |
| } |