| // 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()); |
| FileSystemUtils.deleteTreesBelow(testRoot); |
| 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(null, actionKeyContext, outErr, testRoot, null, 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(); |
| } |
| } |