| // 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.truth.Truth.assertThat; | |
| import static org.junit.Assert.fail; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.base.Throwables; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| 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.ExecException; | |
| import com.google.devtools.build.lib.actions.ExecutionStrategy; | |
| import com.google.devtools.build.lib.actions.ExecutorInitException; | |
| import com.google.devtools.build.lib.actions.LocalHostCapacity; | |
| import com.google.devtools.build.lib.actions.ResourceManager; | |
| import com.google.devtools.build.lib.actions.ResourceSet; | |
| import com.google.devtools.build.lib.actions.SandboxedSpawnActionContext; | |
| 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.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.ExecutionPolicy; | |
| 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.FileSystem; | |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.vfs.Root; | |
| import com.google.devtools.build.lib.vfs.util.FileSystems; | |
| import java.io.IOException; | |
| import java.util.List; | |
| import java.util.concurrent.Callable; | |
| 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.AtomicReference; | |
| import java.util.function.Function; | |
| import org.junit.After; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Tests for {@link DynamicSpawnStrategy}. */ | |
| @RunWith(JUnit4.class) | |
| public class DynamicSpawnStrategyTest { | |
| protected FileSystem fileSystem; | |
| protected Path testRoot; | |
| private ExecutorService executorService; | |
| private MockLocalSpawnStrategy localStrategy; | |
| private MockRemoteSpawnStrategy remoteStrategy; | |
| private SpawnActionContext dynamicSpawnStrategy; | |
| private Artifact inputArtifact; | |
| private Artifact outputArtifact; | |
| private FileOutErr outErr; | |
| private ActionExecutionContext actionExecutionContext; | |
| private DynamicExecutionOptions options; | |
| private final ActionKeyContext actionKeyContext = new ActionKeyContext(); | |
| abstract static class MockSpawnStrategy implements SandboxedSpawnActionContext { | |
| private final Path testRoot; | |
| private final int delayMs; | |
| private volatile Spawn executedSpawn; | |
| private CountDownLatch succeeded = new CountDownLatch(1); | |
| private boolean failsDuringExecution; | |
| private CountDownLatch beforeExecutionWaitFor; | |
| private Callable<List<SpawnResult>> execute; | |
| public MockSpawnStrategy(Path testRoot, int delayMs) { | |
| this.testRoot = testRoot; | |
| this.delayMs = delayMs; | |
| } | |
| @Override | |
| public List<SpawnResult> exec(Spawn spawn, ActionExecutionContext actionExecutionContext) | |
| throws ExecException, InterruptedException { | |
| return exec(spawn, actionExecutionContext, null); | |
| } | |
| @Override | |
| public boolean canExec(Spawn spawn) { | |
| return true; | |
| } | |
| @Override | |
| public List<SpawnResult> exec( | |
| Spawn spawn, | |
| ActionExecutionContext actionExecutionContext, | |
| AtomicReference<Class<? extends SpawnActionContext>> writeOutputFiles) | |
| throws ExecException, InterruptedException { | |
| executedSpawn = spawn; | |
| if (beforeExecutionWaitFor != null) { | |
| beforeExecutionWaitFor.countDown(); | |
| beforeExecutionWaitFor.await(); | |
| } | |
| if (delayMs > 0) { | |
| Thread.sleep(delayMs); | |
| } | |
| List<SpawnResult> spawnResults = ImmutableList.of(); | |
| if (execute != null) { | |
| try { | |
| spawnResults = execute.call(); | |
| } catch (ExecException | InterruptedException e) { | |
| throw e; | |
| } catch (Exception e) { | |
| Throwables.throwIfUnchecked(e); | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| if (failsDuringExecution) { | |
| try { | |
| FileSystemUtils.appendIsoLatin1( | |
| actionExecutionContext.getFileOutErr().getOutputPath(), | |
| "action failed with " + getClass().getSimpleName()); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| throw new UserExecException(getClass().getSimpleName() + " failed to execute the Spawn"); | |
| } | |
| if (writeOutputFiles != null && !writeOutputFiles.compareAndSet(null, getClass())) { | |
| throw new InterruptedException(getClass() + " could not acquire barrier"); | |
| } else { | |
| for (ActionInput output : spawn.getOutputFiles()) { | |
| try { | |
| FileSystemUtils.writeIsoLatin1( | |
| testRoot.getRelative(output.getExecPath()), getClass().getSimpleName()); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| } | |
| try { | |
| FileSystemUtils.appendIsoLatin1( | |
| actionExecutionContext.getFileOutErr().getOutputPath(), | |
| "output files written with " + getClass().getSimpleName()); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| succeeded.countDown(); | |
| return spawnResults; | |
| } | |
| public Spawn getExecutedSpawn() { | |
| return executedSpawn; | |
| } | |
| boolean succeeded() { | |
| return succeeded.getCount() == 0; | |
| } | |
| CountDownLatch getSucceededLatch() { | |
| return succeeded; | |
| } | |
| public void failsDuringExecution() { | |
| failsDuringExecution = true; | |
| } | |
| public void beforeExecutionWaitFor(CountDownLatch countDownLatch) { | |
| beforeExecutionWaitFor = countDownLatch; | |
| } | |
| void setExecute(Callable<List<SpawnResult>> execute) { | |
| this.execute = execute; | |
| } | |
| } | |
| @ExecutionStrategy( | |
| name = {"mock-remote"}, | |
| contextType = SpawnActionContext.class | |
| ) | |
| private static class MockRemoteSpawnStrategy extends MockSpawnStrategy { | |
| public MockRemoteSpawnStrategy(Path testRoot, int delayMs) { | |
| super(testRoot, delayMs); | |
| } | |
| } | |
| @ExecutionStrategy( | |
| name = {"mock-local"}, | |
| contextType = SpawnActionContext.class | |
| ) | |
| private static class MockLocalSpawnStrategy extends MockSpawnStrategy { | |
| public MockLocalSpawnStrategy(Path testRoot, int delayMs) { | |
| super(testRoot, delayMs); | |
| } | |
| } | |
| private static class DynamicSpawnStrategyUnderTest extends DynamicSpawnStrategy { | |
| public DynamicSpawnStrategyUnderTest( | |
| ExecutorService executorService, | |
| DynamicExecutionOptions options, | |
| Function<Spawn, ExecutionPolicy> executionPolicy) { | |
| super(executorService, options, executionPolicy); | |
| } | |
| } | |
| @Before | |
| public void setUp() throws Exception { | |
| ResourceManager.instance().setAvailableResources(LocalHostCapacity.getLocalHostCapacity()); | |
| ResourceManager.instance() | |
| .setRamUtilizationPercentage(ResourceManager.DEFAULT_RAM_UTILIZATION_PERCENTAGE); | |
| ResourceManager.instance().resetResourceUsage(); | |
| fileSystem = FileSystems.getNativeFileSystem(); | |
| testRoot = fileSystem.getPath(TestUtils.tmpDir()); | |
| testRoot.deleteTreesBelow(); | |
| executorService = Executors.newCachedThreadPool(); | |
| inputArtifact = | |
| new Artifact( | |
| PathFragment.create("input.txt"), ArtifactRoot.asSourceRoot(Root.fromPath(testRoot))); | |
| outputArtifact = | |
| new Artifact( | |
| PathFragment.create("output.txt"), ArtifactRoot.asSourceRoot(Root.fromPath(testRoot))); | |
| outErr = new FileOutErr(testRoot.getRelative("stdout"), testRoot.getRelative("stderr")); | |
| actionExecutionContext = | |
| ActionsTestUtil.createContext( | |
| /*executor=*/ null, | |
| /*eventHandler=*/ null, | |
| actionKeyContext, | |
| outErr, | |
| testRoot, | |
| /*metadataHandler=*/ null, | |
| /*actionGraph=*/ null); | |
| } | |
| void createSpawnStrategy(int localDelay, int remoteDelay) throws ExecutorInitException { | |
| localStrategy = new MockLocalSpawnStrategy(testRoot, localDelay); | |
| remoteStrategy = new MockRemoteSpawnStrategy(testRoot, remoteDelay); | |
| options = new DynamicExecutionOptions(); | |
| options.dynamicLocalStrategy = "mock-local"; | |
| options.dynamicRemoteStrategy = "mock-remote"; | |
| options.dynamicWorkerStrategy = "mock-local"; | |
| options.internalSpawnScheduler = true; | |
| options.localExecutionDelay = 0; | |
| dynamicSpawnStrategy = | |
| new DynamicSpawnStrategyUnderTest(executorService, options, this::getExecutionPolicy); | |
| dynamicSpawnStrategy.executorCreated(ImmutableList.of(localStrategy, remoteStrategy)); | |
| } | |
| ExecutionPolicy getExecutionPolicy(Spawn spawn) { | |
| if (spawn.getExecutionInfo().containsKey("local")) { | |
| return ExecutionPolicy.LOCAL_EXECUTION_ONLY; | |
| } else if (spawn.getExecutionInfo().containsKey("remote")) { | |
| return ExecutionPolicy.REMOTE_EXECUTION_ONLY; | |
| } else { | |
| return ExecutionPolicy.ANYWHERE; | |
| } | |
| } | |
| @After | |
| public void tearDown() throws Exception { | |
| executorService.shutdownNow(); | |
| } | |
| Spawn getSpawnForTest(boolean forceLocal, boolean forceRemote) { | |
| Preconditions.checkArgument( | |
| !(forceLocal && forceRemote), "Cannot force local and remote at the same time"); | |
| ActionExecutionMetadata action = | |
| new NullAction(ImmutableList.of(inputArtifact), outputArtifact); | |
| return new BaseSpawn( | |
| ImmutableList.<String>of(), | |
| ImmutableMap.<String, String>of(), | |
| forceLocal | |
| ? ImmutableMap.of("local", "1") | |
| : forceRemote ? ImmutableMap.of("remote", "1") : ImmutableMap.<String, String>of(), | |
| action, | |
| ResourceSet.create(1, 0, 0)); | |
| } | |
| @Test | |
| public void nonRemotableSpawnRunsLocally() throws Exception { | |
| Spawn spawn = getSpawnForTest(true, false); | |
| createSpawnStrategy(0, 0); | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| 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 nonLocallyExecutableSpawnRunsRemotely() throws Exception { | |
| Spawn spawn = getSpawnForTest(false, true); | |
| createSpawnStrategy(0, 0); | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| assertThat(localStrategy.getExecutedSpawn()).isNull(); | |
| 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 actionSucceedsIfLocalExecutionSucceedsEvenIfRemoteFailsLater() throws Exception { | |
| Spawn spawn = getSpawnForTest(false, false); | |
| createSpawnStrategy(0, 2000); | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| localStrategy.beforeExecutionWaitFor(countDownLatch); | |
| remoteStrategy.beforeExecutionWaitFor(countDownLatch); | |
| remoteStrategy.failsDuringExecution(); | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| 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 { | |
| Spawn spawn = getSpawnForTest(false, false); | |
| createSpawnStrategy(2000, 0); | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| localStrategy.beforeExecutionWaitFor(countDownLatch); | |
| localStrategy.failsDuringExecution(); | |
| remoteStrategy.beforeExecutionWaitFor(countDownLatch); | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| 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 { | |
| Spawn spawn = getSpawnForTest(false, false); | |
| createSpawnStrategy(0, 2000); | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| localStrategy.beforeExecutionWaitFor(countDownLatch); | |
| localStrategy.failsDuringExecution(); | |
| remoteStrategy.beforeExecutionWaitFor(countDownLatch); | |
| try { | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| fail("Expected dynamicSpawnStrategy to throw an ExecException"); | |
| } catch (ExecException e) { | |
| 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 { | |
| Spawn spawn = getSpawnForTest(false, false); | |
| createSpawnStrategy(2000, 0); | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| localStrategy.beforeExecutionWaitFor(countDownLatch); | |
| remoteStrategy.beforeExecutionWaitFor(countDownLatch); | |
| remoteStrategy.failsDuringExecution(); | |
| try { | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| fail("Expected dynamicSpawnStrategy to throw an ExecException"); | |
| } catch (ExecException e) { | |
| 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 { | |
| Spawn spawn = getSpawnForTest(false, false); | |
| createSpawnStrategy(0, 0); | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| localStrategy.beforeExecutionWaitFor(countDownLatch); | |
| remoteStrategy.beforeExecutionWaitFor(countDownLatch); | |
| localStrategy.failsDuringExecution(); | |
| remoteStrategy.failsDuringExecution(); | |
| try { | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| fail("Expected dynamicSpawnStrategy to throw an ExecException"); | |
| } catch (ExecException e) { | |
| 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 noDeadlockWithSingleThreadedExecutor() throws Exception { | |
| final Spawn spawn = getSpawnForTest(/*forceLocal=*/ false, /*forceRemote=*/ false); | |
| // Replace the executorService with a single threaded one. | |
| executorService = Executors.newSingleThreadExecutor(); | |
| createSpawnStrategy(/*localDelay=*/ 0, /*remoteDelay=*/ 0); | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| 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 { | |
| final Spawn spawn = getSpawnForTest(false, false); | |
| createSpawnStrategy(60000, 60000); | |
| CountDownLatch countDownLatch = new CountDownLatch(2); | |
| localStrategy.beforeExecutionWaitFor(countDownLatch); | |
| remoteStrategy.beforeExecutionWaitFor(countDownLatch); | |
| TestThread testThread = | |
| new TestThread() { | |
| @Override | |
| public void runTest() throws Exception { | |
| try { | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| } 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(); | |
| } | |
| private void strategyWaitsForBothSpawnsToFinish(boolean interruptThread, boolean executionFails) | |
| throws Exception { | |
| final Spawn spawn = getSpawnForTest(false, false); | |
| createSpawnStrategy(0, 0); | |
| CountDownLatch waitToFinish = new CountDownLatch(1); | |
| CountDownLatch wasInterrupted = new CountDownLatch(1); | |
| CountDownLatch executionCanProceed = new CountDownLatch(2); | |
| localStrategy.setExecute( | |
| () -> { | |
| executionCanProceed.countDown(); | |
| try { | |
| Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| throw new IllegalStateException("Should have been interrupted"); | |
| } catch (InterruptedException e) { | |
| // Expected. | |
| } | |
| wasInterrupted.countDown(); | |
| try { | |
| Preconditions.checkState( | |
| waitToFinish.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)); | |
| } catch (InterruptedException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| return ImmutableList.of(); | |
| }); | |
| if (executionFails) { | |
| remoteStrategy.failsDuringExecution(); | |
| } | |
| remoteStrategy.beforeExecutionWaitFor(executionCanProceed); | |
| TestThread testThread = | |
| new TestThread() { | |
| @Override | |
| public void runTest() { | |
| try { | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| Preconditions.checkState(!interruptThread && !executionFails); | |
| } catch (InterruptedException e) { | |
| Preconditions.checkState(interruptThread && !executionFails); | |
| Preconditions.checkState(!Thread.currentThread().isInterrupted()); | |
| } catch (ExecException e) { | |
| Preconditions.checkState(executionFails); | |
| Preconditions.checkState(Thread.currentThread().isInterrupted() == interruptThread); | |
| } | |
| } | |
| }; | |
| testThread.start(); | |
| if (!executionFails) { | |
| assertThat( | |
| remoteStrategy | |
| .getSucceededLatch() | |
| .await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) | |
| .isTrue(); | |
| } | |
| assertThat(wasInterrupted.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue(); | |
| assertThat(testThread.isAlive()).isTrue(); | |
| if (interruptThread) { | |
| testThread.interrupt(); | |
| } | |
| // Wait up to 5 seconds for this thread to finish. It should not have finished. | |
| testThread.join(5000); | |
| assertThat(testThread.isAlive()).isTrue(); | |
| waitToFinish.countDown(); | |
| testThread.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| } | |
| @Test | |
| public void strategyWaitsForBothSpawnsToFinish() throws Exception { | |
| strategyWaitsForBothSpawnsToFinish(false, false); | |
| } | |
| @Test | |
| public void strategyWaitsForBothSpawnsToFinishEvenIfInterrupted() throws Exception { | |
| strategyWaitsForBothSpawnsToFinish(true, false); | |
| } | |
| @Test | |
| public void strategyWaitsForBothSpawnsToFinishOnFailure() throws Exception { | |
| strategyWaitsForBothSpawnsToFinish(false, true); | |
| } | |
| @Test | |
| public void strategyWaitsForBothSpawnsToFinishOnFailureEvenIfInterrupted() throws Exception { | |
| strategyWaitsForBothSpawnsToFinish(true, true); | |
| } | |
| @Test | |
| public void strategyPropagatesFasterLocalException() throws Exception { | |
| strategyPropagatesException(true); | |
| } | |
| @Test | |
| public void strategyPropagatesFasterRemoteException() throws Exception { | |
| strategyPropagatesException(false); | |
| } | |
| private void strategyPropagatesException(boolean preferLocal) throws Exception { | |
| final Spawn spawn = getSpawnForTest(false, false); | |
| createSpawnStrategy(!preferLocal ? 60000 : 0, preferLocal ? 60000 : 0); | |
| String message = "Mock spawn execution exception"; | |
| Callable<List<SpawnResult>> execute = () -> { | |
| throw new IllegalStateException(message); | |
| }; | |
| localStrategy.setExecute(execute); | |
| remoteStrategy.setExecute(execute); | |
| try { | |
| dynamicSpawnStrategy.exec(spawn, actionExecutionContext); | |
| fail("Expected dynamicSpawnStrategy to throw an ExecException"); | |
| } catch (ExecException e) { | |
| assertThat(e).hasMessageThat().matches("java.lang.IllegalStateException: " + message); | |
| } | |
| Spawn executedSpawn = localStrategy.getExecutedSpawn(); | |
| executedSpawn = executedSpawn == null ? remoteStrategy.getExecutedSpawn() : executedSpawn; | |
| assertThat(executedSpawn).isEqualTo(spawn); | |
| assertThat(localStrategy.succeeded()).isFalse(); | |
| assertThat(remoteStrategy.succeeded()).isFalse(); | |
| } | |
| } |