/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.api.workspace.server; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Futures; import org.eclipse.che.api.agent.server.AgentRegistry; import org.eclipse.che.api.agent.server.impl.AgentSorter; import org.eclipse.che.api.agent.server.launcher.AgentLauncherFactory; import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.model.machine.MachineConfig; import org.eclipse.che.api.core.model.machine.MachineStatus; import org.eclipse.che.api.core.model.workspace.Environment; import org.eclipse.che.api.core.model.workspace.ExtendedMachine; import org.eclipse.che.api.core.model.workspace.Workspace; import org.eclipse.che.api.core.model.workspace.WorkspaceStatus; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.environment.server.CheEnvironmentEngine; import org.eclipse.che.api.environment.server.NoOpMachineInstance; import org.eclipse.che.api.environment.server.exception.EnvironmentException; import org.eclipse.che.api.environment.server.exception.EnvironmentNotRunningException; import org.eclipse.che.api.environment.server.exception.EnvironmentStartInterruptedException; import org.eclipse.che.api.machine.server.exception.SnapshotException; import org.eclipse.che.api.machine.server.model.impl.MachineConfigImpl; import org.eclipse.che.api.machine.server.model.impl.MachineImpl; import org.eclipse.che.api.machine.server.model.impl.MachineLimitsImpl; import org.eclipse.che.api.machine.server.model.impl.MachineRuntimeInfoImpl; import org.eclipse.che.api.machine.server.model.impl.MachineSourceImpl; import org.eclipse.che.api.machine.server.model.impl.SnapshotImpl; import org.eclipse.che.api.machine.server.spi.Instance; import org.eclipse.che.api.machine.server.spi.SnapshotDao; import org.eclipse.che.api.workspace.server.WorkspaceRuntimes.RuntimeState; import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl; import org.eclipse.che.api.workspace.server.model.impl.ExtendedMachineImpl; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceRuntimeImpl; import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent; import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent.EventType; import org.eclipse.che.dto.server.DtoFactory; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Listeners; import org.testng.annotations.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import static java.lang.String.format; import static java.util.Collections.singletonList; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; /** * @author Yevhenii Voevodin * @author Alexander Garagatyi */ @Listeners(MockitoTestNGListener.class) public class WorkspaceRuntimesTest { @Mock private EventService eventService; @Mock private CheEnvironmentEngine envEngine; @Mock private AgentSorter agentSorter; @Mock private AgentLauncherFactory launcherFactory; @Mock private AgentRegistry agentRegistry; @Mock private WorkspaceSharedPool sharedPool; @Mock private SnapshotDao snapshotDao; @Mock private Future<WorkspaceRuntimeImpl> runtimeFuture; @Mock private WorkspaceRuntimes.StartTask startTask; @Captor private ArgumentCaptor<WorkspaceStatusEvent> eventCaptor; @Captor private ArgumentCaptor<Callable<?>> taskCaptor; @Captor private ArgumentCaptor<Collection<SnapshotImpl>> snapshotsCaptor; private WorkspaceRuntimes runtimes; private ConcurrentMap<String, RuntimeState> runtimeStates; @BeforeMethod public void setUp() throws Exception { MockitoAnnotations.initMocks(this); runtimes = new WorkspaceRuntimes(eventService, envEngine, agentSorter, launcherFactory, agentRegistry, snapshotDao, sharedPool, runtimeStates = new ConcurrentHashMap<>()); } @Test(dataProvider = "allStatuses") public void getsStatus(WorkspaceStatus status) throws Exception { setRuntime("workspace", status); assertEquals(runtimes.getStatus("workspace"), status); } @Test(expectedExceptions = NotFoundException.class, expectedExceptionsMessageRegExp = "Workspace with id 'non_running' is not running") public void throwsNotFoundExceptionWhenGettingNonExistingRuntime() throws Exception { runtimes.getRuntime("non_running"); } @Test public void returnsStoppedStatusWhenWorkspaceIsNotRunning() throws Exception { assertEquals(runtimes.getStatus("not_running"), WorkspaceStatus.STOPPED); } @Test public void getsRuntime() throws Exception { setRuntime("workspace", WorkspaceStatus.RUNNING, "env-name"); List<Instance> machines = prepareMachines("workspace", "env-name"); assertEquals(runtimes.getRuntime("workspace"), new WorkspaceRuntimeImpl("env-name", machines)); verify(envEngine).getMachines("workspace"); } @Test public void hasRuntime() { setRuntime("workspace", WorkspaceStatus.STARTING); assertTrue(runtimes.hasRuntime("workspace")); } @Test public void doesNotHaveRuntime() { assertFalse(runtimes.hasRuntime("not_running")); } @Test public void injectsRuntime() throws Exception { setRuntime("workspace", WorkspaceStatus.RUNNING, "env-name"); List<Instance> machines = prepareMachines("workspace", "env-name"); WorkspaceImpl workspace = WorkspaceImpl.builder() .setId("workspace") .build(); runtimes.injectRuntime(workspace); assertEquals(workspace.getStatus(), WorkspaceStatus.RUNNING); assertEquals(workspace.getRuntime(), new WorkspaceRuntimeImpl("env-name", machines)); } @Test public void injectsStoppedStatusWhenWorkspaceDoesNotHaveRuntime() throws Exception { WorkspaceImpl workspace = WorkspaceImpl.builder() .setId("workspace") .build(); runtimes.injectRuntime(workspace); assertEquals(workspace.getStatus(), WorkspaceStatus.STOPPED); assertNull(workspace.getRuntime()); } @Test public void injectsStatusAndEmptyMachinesWhenCanNotGetEnvironmentMachines() throws Exception { setRuntime("workspace", WorkspaceStatus.RUNNING, "env-name"); setNoMachinesForWorkspace("workspace"); WorkspaceImpl workspace = WorkspaceImpl.builder() .setId("workspace") .build(); runtimes.injectRuntime(workspace); assertEquals(workspace.getStatus(), WorkspaceStatus.RUNNING); assertEquals(workspace.getRuntime().getActiveEnv(), "env-name"); assertTrue(workspace.getRuntime().getMachines().isEmpty()); } @Test public void startsWorkspace() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); List<Instance> machines = allowEnvironmentStart(workspace, "env-name"); prepareMachines(workspace.getId(), machines); CompletableFuture<WorkspaceRuntimeImpl> cmpFuture = runtimes.startAsync(workspace, "env-name", false); captureAsyncTaskAndExecuteSynchronously(); WorkspaceRuntimeImpl runtime = cmpFuture.get(); assertEquals(runtimes.getStatus(workspace.getId()), WorkspaceStatus.RUNNING); assertEquals(runtime.getActiveEnv(), "env-name"); assertEquals(runtime.getMachines().size(), machines.size()); verifyEventsSequence(event("workspace", WorkspaceStatus.STOPPED, WorkspaceStatus.STARTING, EventType.STARTING, null), event("workspace", WorkspaceStatus.STARTING, WorkspaceStatus.RUNNING, EventType.RUNNING, null)); } @Test(expectedExceptions = ConflictException.class, expectedExceptionsMessageRegExp = "Could not start workspace 'test-workspace' because its status is 'RUNNING'") public void throwsConflictExceptionWhenWorkspaceIsRunning() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); setRuntime(workspace.getId(), WorkspaceStatus.RUNNING); runtimes.startAsync(workspace, "env-name", false); } @Test public void cancelsWorkspaceStartIfEnvironmentStartIsInterrupted() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); rejectEnvironmentStart(workspace, "env-name", new EnvironmentStartInterruptedException(workspace.getId(), "env-name")); CompletableFuture<WorkspaceRuntimeImpl> cmpFuture = runtimes.startAsync(workspace, "env-name", false); captureAndVerifyRuntimeStateAfterInterruption(workspace, cmpFuture); } @Test public void failsWorkspaceStartWhenEnvironmentStartIsFailed() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); rejectEnvironmentStart(workspace, "env-name", new EnvironmentException("no no no!")); CompletableFuture<WorkspaceRuntimeImpl> cmpFuture = runtimes.startAsync(workspace, "env-name", false); try { captureAsyncTaskAndExecuteSynchronously(); } catch (EnvironmentException x) { assertEquals(x.getMessage(), "no no no!"); verifyCompletionException(cmpFuture, EnvironmentException.class, "no no no!"); } assertFalse(runtimes.hasRuntime(workspace.getId())); verifyEventsSequence(event("workspace", WorkspaceStatus.STOPPED, WorkspaceStatus.STARTING, EventType.STARTING, null), event("workspace", WorkspaceStatus.STARTING, WorkspaceStatus.STOPPED, EventType.ERROR, "Start of environment 'env-name' failed. Error: no no no!")); } @Test public void interruptsStartAfterEnvironmentIsStartedButRuntimeStatusIsNotRunning() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); // let's say status is changed to STOPPING by stop method, // but starting thread hasn't been interrupted yet allowEnvironmentStart(workspace, "env-name", () -> setRuntime("workspace", WorkspaceStatus.STOPPING)); CompletableFuture<WorkspaceRuntimeImpl> cmpFuture = runtimes.startAsync(workspace, "env-name", false); captureAndVerifyRuntimeStateAfterInterruption(workspace, cmpFuture); verifyEventsSequence(event("workspace", WorkspaceStatus.STOPPED, WorkspaceStatus.STARTING, EventType.STARTING, null), event("workspace", WorkspaceStatus.STARTING, WorkspaceStatus.STOPPING, EventType.STOPPING, null), event("workspace", WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED, EventType.STOPPED, null)); verify(envEngine).stop(workspace.getId()); } @Test public void interruptsStartAfterEnvironmentIsStartedButThreadIsInterrupted() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); // the status is successfully updated from STARTING -> RUNNING but after // that thread is interrupted so #stop is waiting for starting thread to stop the environment allowEnvironmentStart(workspace, "env-name", () -> Thread.currentThread().interrupt()); CompletableFuture<WorkspaceRuntimeImpl> cmpFuture = runtimes.startAsync(workspace, "env-name", false); captureAndVerifyRuntimeStateAfterInterruption(workspace, cmpFuture); verifyEventsSequence(event("workspace", WorkspaceStatus.STOPPED, WorkspaceStatus.STARTING, EventType.STARTING, null), event("workspace", WorkspaceStatus.STARTING, WorkspaceStatus.STOPPING, EventType.STOPPING, null), event("workspace", WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED, EventType.STOPPED, null)); verify(envEngine).stop(workspace.getId()); } @Test public void throwsStartInterruptedExceptionWhenStartIsInterruptedAndEnvironmentStopIsFailed() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); // let's say status is changed to STOPPING by stop method, // but starting thread hasn't been interrupted yet allowEnvironmentStart(workspace, "env-name", () -> Thread.currentThread().interrupt()); rejectEnvironmentStop(workspace, new ServerException("no!")); CompletableFuture<WorkspaceRuntimeImpl> cmpFuture = runtimes.startAsync(workspace, "env-name", false); captureAndVerifyRuntimeStateAfterInterruption(workspace, cmpFuture); verifyEventsSequence(event("workspace", WorkspaceStatus.STOPPED, WorkspaceStatus.STARTING, EventType.STARTING, null), event("workspace", WorkspaceStatus.STARTING, WorkspaceStatus.STOPPING, EventType.STOPPING, null), event("workspace", WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED, EventType.ERROR, "no!")); verify(envEngine).stop(workspace.getId()); } @Test public void releasesClientsWhoWaitForStartTaskResultAndTaskIsCompleted() throws Exception { ExecutorService pool = Executors.newCachedThreadPool(); CountDownLatch releasedLatch = new CountDownLatch(5); // this thread + 5 awaiting threads CyclicBarrier callTaskBarrier = new CyclicBarrier(6); WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); allowEnvironmentStart(workspace, "env-name"); // the action runtimes.startAsync(workspace, "env-name", false); // register waiters for (int i = 0; i < 5; i++) { WorkspaceRuntimes.StartTask startTask = runtimeStates.get(workspace.getId()).startTask; pool.submit(() -> { // wait all the task to meet this barrier callTaskBarrier.await(); // wait for start task to finish startTask.await(); // good, release a part releasedLatch.countDown(); return null; }); } callTaskBarrier.await(); captureAsyncTaskAndExecuteSynchronously(); try { assertTrue(releasedLatch.await(2, TimeUnit.SECONDS), "start task wait clients are not released"); } finally { shutdownAndWaitPool(pool); } } @Test public void stopsRunningWorkspace() throws Exception { setRuntime("workspace", WorkspaceStatus.RUNNING); runtimes.stop("workspace"); verify(envEngine).stop("workspace"); verifyEventsSequence(event("workspace", WorkspaceStatus.RUNNING, WorkspaceStatus.STOPPING, EventType.STOPPING, null), event("workspace", WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED, EventType.STOPPED, null)); assertFalse(runtimeStates.containsKey("workspace")); } @Test public void stopsTheRunningWorkspaceWhileServerExceptionOccurs() throws Exception { setRuntime("workspace", WorkspaceStatus.RUNNING); doThrow(new ServerException("no!")).when(envEngine).stop("workspace"); try { runtimes.stop("workspace"); } catch (ServerException x) { assertEquals(x.getMessage(), "no!"); } verify(envEngine).stop("workspace"); assertFalse(runtimeStates.containsKey("workspace")); verifyEventsSequence(event("workspace", WorkspaceStatus.RUNNING, WorkspaceStatus.STOPPING, EventType.STOPPING, null), event("workspace", WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED, EventType.ERROR, "no!")); } @Test public void stopsTheRunningWorkspaceAndRethrowsTheErrorDifferentFromServerException() throws Exception { setRuntime("workspace", WorkspaceStatus.RUNNING); doThrow(new EnvironmentNotRunningException("no!")).when(envEngine).stop("workspace"); try { runtimes.stop("workspace"); } catch (ServerException x) { assertEquals(x.getMessage(), "no!"); assertTrue(x.getCause() instanceof EnvironmentNotRunningException); } verify(envEngine).stop("workspace"); assertFalse(runtimeStates.containsKey("workspace")); verifyEventsSequence(event("workspace", WorkspaceStatus.RUNNING, WorkspaceStatus.STOPPING, EventType.STOPPING, null), event("workspace", WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED, EventType.ERROR, "no!")); } @Test public void cancellationOfPendingStartTask() throws Throwable { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); when(sharedPool.submit(any())).thenReturn(Futures.immediateFuture(null)); CompletableFuture<WorkspaceRuntimeImpl> cmpFuture = runtimes.startAsync(workspace, "env-name", false); // the real start is not being executed, fake sharedPool suppressed it // so the situation is the same to the one if the task is cancelled before // executor service started executing it runtimes.stop(workspace.getId()); // start awaiting clients MUST receive interruption try { cmpFuture.get(); } catch (ExecutionException x) { verifyCompletionException(cmpFuture, EnvironmentStartInterruptedException.class, "Start of environment 'env-name' in workspace 'workspace' is interrupted"); } // if there is a state when the future is being cancelled, // and start task is marked as used, executor must not execute the // task but throw cancellation exception instead, once start task is // completed clients receive interrupted exception and cancellation doesn't bother them try { captureAsyncTaskAndExecuteSynchronously(); } catch (CancellationException cancelled) { assertEquals(cancelled.getMessage(), "Start of the workspace 'workspace' was cancelled"); } verifyEventsSequence(event("workspace", WorkspaceStatus.STOPPED, WorkspaceStatus.STARTING, EventType.STARTING, null), event("workspace", WorkspaceStatus.STARTING, WorkspaceStatus.STOPPING, EventType.STOPPING, null), event("workspace", WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED, EventType.STOPPED, null)); } @Test public void cancellationOfRunningStartTask() throws Exception { setRuntime("workspace", WorkspaceStatus.STARTING, "env-name", runtimeFuture, startTask); doThrow(new EnvironmentStartInterruptedException("workspace", "env-name")).when(startTask).await(); runtimes.stop("workspace"); verify(runtimeFuture).cancel(true); verify(startTask).await(); } @Test(expectedExceptions = NotFoundException.class, expectedExceptionsMessageRegExp = "Workspace with id 'workspace' is not running") public void throwsNotFoundExceptionWhenStoppingNotRunningWorkspace() throws Exception { runtimes.stop("workspace"); } @Test(expectedExceptions = ConflictException.class, expectedExceptionsMessageRegExp = "Couldn't stop the workspace 'workspace' because its status is '.*'.*", dataProvider = "notAllowedToStopStatuses") public void doesNotStopTheWorkspaceWhenStatusIsWrong(WorkspaceStatus status) throws Exception { setRuntime("workspace", status); runtimes.stop("workspace"); } @Test public void shutdown() throws Exception { setRuntime("workspace", WorkspaceStatus.RUNNING, "env-name"); runtimes.shutdown(); assertFalse(runtimes.hasRuntime("workspace")); verify(envEngine).stop("workspace"); } @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Workspace runtimes service shutdown has been already called") public void throwsExceptionWhenShutdownCalledTwice() throws Exception { runtimes.shutdown(); runtimes.shutdown(); } @Test public void startedRuntimeAndReturnedFromGetMethodAreTheSame() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); allowEnvironmentStart(workspace, "env-name"); prepareMachines(workspace.getId(), "env-name"); CompletableFuture<WorkspaceRuntimeImpl> cmpFuture = runtimes.startAsync(workspace, "env-name", false); captureAsyncTaskAndExecuteSynchronously(); assertEquals(cmpFuture.get(), runtimes.getRuntime(workspace.getId())); } @Test public void shouldBeAbleToStartMachine() throws Exception { // when setRuntime("workspace", WorkspaceStatus.RUNNING, "env-name"); MachineConfig config = newMachine("workspace", "env-name", "new", false).getConfig(); Instance instance = mock(Instance.class); when(envEngine.startMachine(anyString(), any(MachineConfig.class), any())).thenReturn(instance); when(instance.getConfig()).thenReturn(config); // when Instance actual = runtimes.startMachine("workspace", config); // then assertEquals(actual, instance); verify(envEngine).startMachine(eq("workspace"), eq(config), any()); } @Test(expectedExceptions = NotFoundException.class, expectedExceptionsMessageRegExp = "Workspace with id '.*' is not running") public void shouldNotStartMachineIfEnvironmentIsNotRunning() throws Exception { // when runtimes.startMachine("someWsID", mock(MachineConfig.class)); // then verify(envEngine, never()).startMachine(anyString(), any(MachineConfig.class), any()); } @Test public void shouldBeAbleToStopMachine() throws Exception { // when setRuntime("workspace", WorkspaceStatus.RUNNING); // when runtimes.stopMachine("workspace", "testMachineId"); // then verify(envEngine).stopMachine("workspace", "testMachineId"); } @Test(expectedExceptions = NotFoundException.class, expectedExceptionsMessageRegExp = "Workspace with id 'someWsID' is not running") public void shouldNotStopMachineIfEnvironmentIsNotRunning() throws Exception { // when runtimes.stopMachine("someWsID", "someMachineId"); // then verify(envEngine, never()).stopMachine(anyString(), anyString()); } @Test public void shouldBeAbleToGetMachine() throws Exception { // given Instance expected = newMachine("workspace", "env-name", "existing", false); when(envEngine.getMachine("workspace", expected.getId())).thenReturn(expected); // when Instance actualMachine = runtimes.getMachine("workspace", expected.getId()); // then assertEquals(actualMachine, expected); verify(envEngine).getMachine("workspace", expected.getId()); } @Test(expectedExceptions = NotFoundException.class, expectedExceptionsMessageRegExp = "test exception") public void shouldThrowExceptionIfGetMachineFromEnvEngineThrowsException() throws Exception { // given Instance expected = newMachine("workspace", "env-name", "existing", false); when(envEngine.getMachine("workspace", expected.getId())) .thenThrow(new NotFoundException("test exception")); // when runtimes.getMachine("workspace", expected.getId()); // then verify(envEngine).getMachine("workspace", expected.getId()); } @Test public void changesStatusFromRunningToSnapshotting() throws Exception { setRuntime("workspace", WorkspaceStatus.RUNNING); runtimes.snapshotAsync("workspace"); assertEquals(runtimes.getStatus("workspace"), WorkspaceStatus.SNAPSHOTTING); } @Test public void changesStatusFromSnapshottingToRunning() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); setRuntime(workspace.getId(), WorkspaceStatus.RUNNING, "env-name"); runtimes.snapshotAsync(workspace.getId()); captureAsyncTaskAndExecuteSynchronously(); assertEquals(runtimes.getStatus(workspace.getId()), WorkspaceStatus.RUNNING); } @Test(expectedExceptions = NotFoundException.class, expectedExceptionsMessageRegExp = "Workspace with id 'non-existing' is not running") public void throwsNotFoundExceptionWhenBeginningSnapshottingForNonExistingWorkspace() throws Exception { runtimes.snapshot("non-existing"); } @Test(expectedExceptions = ConflictException.class, expectedExceptionsMessageRegExp = "Workspace with id '.*' is not 'RUNNING', it's status is 'SNAPSHOTTING'") public void throwsConflictExceptionWhenBeginningSnapshottingForNotRunningWorkspace() throws Exception { setRuntime("workspace", WorkspaceStatus.RUNNING); runtimes.snapshotAsync("workspace"); runtimes.snapshotAsync("workspace"); } @Test(expectedExceptions = ServerException.class, expectedExceptionsMessageRegExp = "can't save") public void failsToCreateSnapshotWhenDevMachineSnapshottingFailed() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); setRuntime(workspace.getId(), WorkspaceStatus.RUNNING); prepareMachines(workspace.getId(), "env-name"); when(envEngine.saveSnapshot(any(), any())).thenThrow(new ServerException("can't save")); try { runtimes.snapshot(workspace.getId()); } catch (Exception x) { verifyEventsSequence(event(workspace.getId(), WorkspaceStatus.RUNNING, WorkspaceStatus.SNAPSHOTTING, EventType.SNAPSHOT_CREATING, null), event(workspace.getId(), WorkspaceStatus.SNAPSHOTTING, WorkspaceStatus.RUNNING, EventType.SNAPSHOT_CREATION_ERROR, "can't save")); throw x; } } @Test public void removesNewlyCreatedSnapshotsWhenFailedToSaveTheirsMetadata() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); setRuntime(workspace.getId(), WorkspaceStatus.RUNNING, "env-name"); doThrow(new SnapshotException("test")).when(snapshotDao) .replaceSnapshots(any(), any(), any()); SnapshotImpl snapshot = mock(SnapshotImpl.class); when(envEngine.saveSnapshot(any(), any())).thenReturn(snapshot); try { runtimes.snapshot(workspace.getId()); } catch (ServerException x) { assertEquals(x.getMessage(), "test"); } verify(snapshotDao).replaceSnapshots(any(), any(), snapshotsCaptor.capture()); verify(envEngine, times(snapshotsCaptor.getValue().size())).removeSnapshot(snapshot); verifyEventsSequence(event(workspace.getId(), WorkspaceStatus.RUNNING, WorkspaceStatus.SNAPSHOTTING, EventType.SNAPSHOT_CREATING, null), event(workspace.getId(), WorkspaceStatus.SNAPSHOTTING, WorkspaceStatus.RUNNING, EventType.SNAPSHOT_CREATION_ERROR, "test")); } @Test public void removesOldSnapshotsWhenNewSnapshotsMetadataSuccessfullySaved() throws Exception { WorkspaceImpl workspace = newWorkspace("workspace", "env-name"); setRuntime(workspace.getId(), WorkspaceStatus.RUNNING); SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); doReturn((singletonList(oldSnapshot))).when(snapshotDao) .replaceSnapshots(any(), any(), any()); runtimes.snapshot(workspace.getId()); verify(envEngine).removeSnapshot(oldSnapshot); verifyEventsSequence(event(workspace.getId(), WorkspaceStatus.RUNNING, WorkspaceStatus.SNAPSHOTTING, EventType.SNAPSHOT_CREATING, null), event(workspace.getId(), WorkspaceStatus.SNAPSHOTTING, WorkspaceStatus.RUNNING, EventType.SNAPSHOT_CREATED, null)); } @Test public void getsRuntimesIds() { setRuntime("workspace1", WorkspaceStatus.STARTING); setRuntime("workspace2", WorkspaceStatus.RUNNING); setRuntime("workspace3", WorkspaceStatus.STOPPING); setRuntime("workspace4", WorkspaceStatus.SNAPSHOTTING); assertEquals(runtimes.getRuntimesIds(), Sets.newHashSet("workspace1", "workspace2", "workspace3", "workspace4")); } @Test public void isAnyRunningReturnsFalseIfThereIsNoSingleRuntime() { assertFalse(runtimes.isAnyRunning()); } @Test public void isAnyRunningReturnsTrueIfThereIsAtLeastOneRunningWorkspace() { setRuntime("workspace1", WorkspaceStatus.STARTING); assertTrue(runtimes.isAnyRunning()); } @Test(expectedExceptions = ConflictException.class, expectedExceptionsMessageRegExp = "Start of the workspace 'test-workspace' is rejected by the system, " + "no more workspaces are allowed to start") public void doesNotAllowToStartWorkspaceIfStartIsRefused() throws Exception { runtimes.refuseWorkspacesStart(); runtimes.startAsync(newWorkspace("workspace1", "env-name"), "env-name", false); } private void captureAsyncTaskAndExecuteSynchronously() throws Exception { verify(sharedPool).submit(taskCaptor.capture()); taskCaptor.getValue().call(); } private void captureAndVerifyRuntimeStateAfterInterruption(Workspace workspace, CompletableFuture<WorkspaceRuntimeImpl> cmpFuture) throws Exception { try { captureAsyncTaskAndExecuteSynchronously(); } catch (EnvironmentStartInterruptedException x) { String expectedMessage = "Start of environment 'env-name' in workspace 'workspace' is interrupted"; assertEquals(x.getMessage(), expectedMessage); verifyCompletionException(cmpFuture, EnvironmentStartInterruptedException.class, expectedMessage); } assertFalse(runtimes.hasRuntime(workspace.getId())); } private void verifyCompletionException(Future<?> f, Class<? extends Exception> expectedEx, String expectedMessage) { assertTrue(f.isDone()); try { f.get(); } catch (ExecutionException execEx) { if (expectedEx.isInstance(execEx.getCause())) { assertEquals(execEx.getCause().getMessage(), expectedMessage); } else { fail(execEx.getMessage(), execEx); } } catch (InterruptedException interruptedEx) { fail(interruptedEx.getMessage(), interruptedEx); } } private void verifyEventsSequence(WorkspaceStatusEvent... expected) { Iterator<WorkspaceStatusEvent> it = captureEvents().iterator(); for (WorkspaceStatusEvent expEvent : expected) { if (!it.hasNext()) { fail(format("It is expected to receive the status changed event '%s' -> '%s' " + "but there are no more events published", expEvent.getPrevStatus(), expEvent.getStatus())); } WorkspaceStatusEvent cur = it.next(); if (cur.getPrevStatus() != expEvent.getPrevStatus() || cur.getStatus() != expEvent.getStatus()) { fail(format("Expected to receive status change '%s' -> '%s', while received '%s' -> '%s'", expEvent.getPrevStatus(), expEvent.getStatus(), cur.getPrevStatus(), cur.getStatus())); } assertEquals(cur, expEvent); } if (it.hasNext()) { WorkspaceStatusEvent next = it.next(); fail(format("No more events expected, but received '%s' -> '%s'", next.getPrevStatus(), next.getStatus())); } } private static WorkspaceStatusEvent event(String workspaceId, WorkspaceStatus prevStatus, WorkspaceStatus status, EventType eventType, String error) { return DtoFactory.newDto(WorkspaceStatusEvent.class) .withWorkspaceId(workspaceId) .withStatus(status) .withPrevStatus(prevStatus) .withEventType(eventType) .withError(error); } private List<WorkspaceStatusEvent> captureEvents() { verify(eventService, atLeastOnce()).publish(eventCaptor.capture()); return eventCaptor.getAllValues(); } private void setRuntime(String workspaceId, WorkspaceStatus status) { runtimeStates.put(workspaceId, new RuntimeState(status, null, null, null)); } private void setRuntime(String workspaceId, WorkspaceStatus status, String envName) { runtimeStates.put(workspaceId, new RuntimeState(status, envName, null, null)); } private void setRuntime(String workspaceId, WorkspaceStatus status, String envName, Future<WorkspaceRuntimeImpl> startFuture, WorkspaceRuntimes.StartTask startTask) { runtimeStates.put(workspaceId, new RuntimeState(status, envName, startTask, startFuture)); } private void setNoMachinesForWorkspace(String workspaceId) throws EnvironmentNotRunningException { when(envEngine.getMachines(workspaceId)).thenThrow(new EnvironmentNotRunningException("")); } private List<Instance> allowEnvironmentStart(Workspace workspace, String envName, TestAction beforeReturn) throws Exception { Environment environment = workspace.getConfig().getEnvironments().get(envName); ArrayList<Instance> machines = new ArrayList<>(environment.getMachines().size()); for (Map.Entry<String, ? extends ExtendedMachine> entry : environment.getMachines().entrySet()) { machines.add(newMachine(workspace.getId(), envName, entry.getKey(), entry.getValue().getAgents().contains("org.eclipse.che.ws-agent"))); } when(envEngine.start(eq(workspace.getId()), eq(envName), eq(workspace.getConfig().getEnvironments().get(envName)), anyBoolean(), any(), any())).thenAnswer(invocation -> { if (beforeReturn != null) { beforeReturn.call(); } return machines; }); return machines; } private List<Instance> allowEnvironmentStart(Workspace workspace, String envName) throws Exception { return allowEnvironmentStart(workspace, envName, null); } private void rejectEnvironmentStart(Workspace workspace, String envName, Exception x) throws Exception { when(envEngine.start(eq(workspace.getId()), eq(envName), eq(workspace.getConfig().getEnvironments().get(envName)), anyBoolean(), any(), any())).thenThrow(x); } private void rejectEnvironmentStop(Workspace workspace, Exception x) throws Exception { doThrow(x).when(envEngine).stop(workspace.getId()); } private List<Instance> prepareMachines(String workspaceId, String envName) throws EnvironmentNotRunningException { List<Instance> machines = new ArrayList<>(3); machines.add(newMachine(workspaceId, envName, "machine1", true)); machines.add(newMachine(workspaceId, envName, "machine2", false)); machines.add(newMachine(workspaceId, envName, "machine3", false)); prepareMachines(workspaceId, machines); return machines; } private void prepareMachines(String workspaceId, List<Instance> machines) throws EnvironmentNotRunningException { when(envEngine.getMachines(workspaceId)).thenReturn(machines); } private Instance newMachine(String workspaceId, String envName, String name, boolean isDev) { MachineImpl machine = MachineImpl.builder() .setConfig(MachineConfigImpl.builder() .setDev(isDev) .setName(name) .setType("docker") .setSource(new MachineSourceImpl("type")) .setLimits(new MachineLimitsImpl(1024)) .build()) .setWorkspaceId(workspaceId) .setEnvName(envName) .setOwner("owner") .setRuntime(new MachineRuntimeInfoImpl(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap())) .setStatus(MachineStatus.RUNNING) .build(); return new NoOpMachineInstance(machine); } private WorkspaceImpl newWorkspace(String workspaceId, String envName) { EnvironmentImpl environment = new EnvironmentImpl(); Map<String, ExtendedMachineImpl> machines = environment.getMachines(); machines.put("dev", new ExtendedMachineImpl(Arrays.asList("org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent"), Collections.emptyMap(), Collections.emptyMap())); machines.put("db", new ExtendedMachineImpl(Arrays.asList("org.eclipse.che.exec", "org.eclipse.che.terminal"), Collections.emptyMap(), Collections.emptyMap())); return WorkspaceImpl.builder() .setId(workspaceId) .setTemporary(false) .setConfig(WorkspaceConfigImpl.builder() .setName("test-workspace") .setDescription("this is test workspace") .setDefaultEnv(envName) .setEnvironments(ImmutableMap.of(envName, environment)) .build()) .build(); } private void shutdownAndWaitPool(ExecutorService pool) throws InterruptedException { pool.shutdownNow(); if (!pool.awaitTermination(10, TimeUnit.SECONDS)) { fail("Can't shutdown test pool"); } } @FunctionalInterface private interface TestAction { void call() throws Exception; } @DataProvider private static Object[][] allStatuses() { WorkspaceStatus[] values = WorkspaceStatus.values(); WorkspaceStatus[][] result = new WorkspaceStatus[values.length][1]; for (int i = 0; i < values.length; i++) { result[i][0] = values[i]; } return result; } @DataProvider private static Object[][] notAllowedToStopStatuses() { return new WorkspaceStatus[][] { {WorkspaceStatus.STOPPING}, {WorkspaceStatus.SNAPSHOTTING} }; } }