diff --git a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/WorkspaceStatus.java b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/WorkspaceStatus.java index 8d71e81c61..2c2b4adcb6 100644 --- a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/WorkspaceStatus.java +++ b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/WorkspaceStatus.java @@ -46,6 +46,18 @@ public enum WorkspaceStatus { */ RUNNING, + /** + * Workspace is in SNAPSHOTTING status if and only if the workspace + * is currently creating snapshots of it's machines. + * + *

Workspace is in SNAPSHOTTING status after it was {@link #RUNNING}. + * The status map: + *

+     *     RUNNING -> SNAPSHOTTING -> RUNNING (normal behaviour/error while snapshotting)
+     * 
+ */ + SNAPSHOTTING, + /** * Workspace considered as stopping if and only if its active environment is shutting down. * diff --git a/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/jpa/JpaSnapshotDao.java b/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/jpa/JpaSnapshotDao.java index 0cd1f7407c..b38644e52a 100644 --- a/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/jpa/JpaSnapshotDao.java +++ b/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/jpa/JpaSnapshotDao.java @@ -23,6 +23,7 @@ import javax.inject.Provider; import javax.inject.Singleton; import javax.persistence.EntityManager; import javax.persistence.NoResultException; +import java.util.Collection; import java.util.List; import static java.lang.String.format; @@ -117,6 +118,20 @@ public class JpaSnapshotDao implements SnapshotDao { } } + @Override + public List replaceSnapshots(String workspaceId, + String envName, + Collection newSnapshots) throws SnapshotException { + requireNonNull(workspaceId, "Required non-null workspace id"); + requireNonNull(envName, "Required non-null environment name"); + requireNonNull(newSnapshots, "Required non-null new snapshots"); + try { + return doReplaceSnapshots(workspaceId, envName, newSnapshots); + } catch (RuntimeException x) { + throw new SnapshotException(x.getLocalizedMessage(), x); + } + } + @Transactional protected void doSave(SnapshotImpl snapshot) { managerProvider.get().persist(snapshot); @@ -131,4 +146,19 @@ public class JpaSnapshotDao implements SnapshotDao { } manager.remove(snapshot); } + + @Transactional + protected List doReplaceSnapshots(String workspaceId, + String envName, + Collection newSnapshots) { + final EntityManager manager = managerProvider.get(); + final List existing = manager.createNamedQuery("Snapshot.findByWorkspaceAndEnvironment", SnapshotImpl.class) + .setParameter("workspaceId", workspaceId) + .setParameter("envName", envName) + .getResultList(); + existing.forEach(manager::remove); + manager.flush(); + newSnapshots.forEach(manager::persist); + return existing; + } } diff --git a/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/model/impl/SnapshotImpl.java b/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/model/impl/SnapshotImpl.java index 70a17b591d..b9a51f503b 100644 --- a/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/model/impl/SnapshotImpl.java +++ b/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/model/impl/SnapshotImpl.java @@ -44,7 +44,12 @@ import java.util.Objects; @NamedQuery(name = "Snapshot.findSnapshots", query = "SELECT snapshot " + "FROM Snapshot snapshot " + - "WHERE snapshot.workspaceId = :workspaceId") + "WHERE snapshot.workspaceId = :workspaceId"), + @NamedQuery(name = "Snapshot.findByWorkspaceAndEnvironment", + query = "SELECT snapshot " + + "FROM Snapshot snapshot " + + "WHERE snapshot.workspaceId = :workspaceId " + + " AND snapshot.envName = :envName") } ) @Table(indexes = @Index(columnList = "workspaceId, envName, machineName", unique = true)) diff --git a/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/spi/SnapshotDao.java b/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/spi/SnapshotDao.java index de8391a576..4575995246 100644 --- a/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/spi/SnapshotDao.java +++ b/wsmaster/che-core-api-machine/src/main/java/org/eclipse/che/api/machine/server/spi/SnapshotDao.java @@ -14,6 +14,7 @@ import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.machine.server.exception.SnapshotException; import org.eclipse.che.api.machine.server.model.impl.SnapshotImpl; +import java.util.Collection; import java.util.List; /** @@ -86,4 +87,24 @@ public interface SnapshotDao { * if other error occur */ void removeSnapshot(String snapshotId) throws NotFoundException, SnapshotException; + + /** + * Replaces all the existing snapshots related to the given workspace + * with a new list of snapshots. + * + * @param workspaceId + * the id of the workspace to replace snapshots + * @param envName + * the name of the environment in workspace with given id + * which is used to search those snapshots that should be replaced + * @param newSnapshots + * the list of the snapshots which will be stored instead of existing ones + * @return the list of replaced(removed/old) snapshots for given workspace and environment, + * or an empty list when there is no a single snapshot for the given workspace + * @throws SnapshotException + * when any error occurs + */ + List replaceSnapshots(String workspaceId, + String envName, + Collection newSnapshots) throws SnapshotException; } diff --git a/wsmaster/che-core-api-machine/src/test/java/org/eclipse/che/api/machine/server/spi/tck/SnapshotDaoTest.java b/wsmaster/che-core-api-machine/src/test/java/org/eclipse/che/api/machine/server/spi/tck/SnapshotDaoTest.java index 2b19130516..651fdad1be 100644 --- a/wsmaster/che-core-api-machine/src/test/java/org/eclipse/che/api/machine/server/spi/tck/SnapshotDaoTest.java +++ b/wsmaster/che-core-api-machine/src/test/java/org/eclipse/che/api/machine/server/spi/tck/SnapshotDaoTest.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.eclipse.che.api.machine.server.spi.tck; +import com.google.common.collect.Sets; import com.google.inject.Inject; import org.eclipse.che.api.core.NotFoundException; @@ -30,6 +31,7 @@ import java.util.HashSet; import java.util.List; import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; @@ -180,6 +182,22 @@ public class SnapshotDaoTest { snapshotDao.removeSnapshot(null); } + @Test(dependsOnMethods = "shouldFindSnapshotsByWorkspaceAndNamespace") + public void replacesSnapshots() throws Exception { + final SnapshotImpl newSnapshot = createSnapshot("new-snapshot", + snapshots[0].getWorkspaceId(), + snapshots[0].getEnvName(), + snapshots[0].getMachineName()); + + final List replaced = snapshotDao.replaceSnapshots(newSnapshot.getWorkspaceId(), + newSnapshot.getEnvName(), + singletonList(newSnapshot)); + + assertEquals(new HashSet<>(replaced), Sets.newHashSet(snapshots[0], snapshots[1])); + assertEquals(new HashSet<>(snapshotDao.findSnapshots(snapshots[0].getWorkspaceId())), + Sets.newHashSet(newSnapshot, snapshots[2])); + } + @DataProvider(name = "missingSnapshots") public Object[][] missingSnapshots() { final SnapshotImpl snapshot = snapshots[0]; diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java index a9c3481e61..c7baf3807b 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java @@ -26,10 +26,11 @@ import org.eclipse.che.api.core.model.workspace.Workspace; import org.eclipse.che.api.core.model.workspace.WorkspaceConfig; import org.eclipse.che.api.core.model.workspace.WorkspaceStatus; import org.eclipse.che.api.core.notification.EventService; -import org.eclipse.che.api.machine.server.spi.SnapshotDao; +import org.eclipse.che.api.machine.server.exception.SnapshotException; import org.eclipse.che.api.machine.server.model.impl.MachineImpl; 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.RuntimeDescriptor; import org.eclipse.che.api.workspace.server.event.WorkspaceCreatedEvent; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; @@ -43,11 +44,15 @@ import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.commons.lang.concurrent.ThreadLocalPropagateContext; import org.eclipse.che.commons.subject.Subject; +import org.eclipse.che.dto.server.DtoFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Named; import javax.inject.Singleton; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; @@ -57,7 +62,9 @@ import static com.google.common.base.MoreObjects.firstNonNull; import static java.lang.Boolean.parseBoolean; import static java.lang.String.format; import static java.lang.System.currentTimeMillis; +import static java.util.Arrays.asList; import static java.util.Collections.emptyMap; +import static java.util.Comparator.comparing; import static java.util.Objects.requireNonNull; import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.RUNNING; import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STOPPED; @@ -67,7 +74,6 @@ import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_STOPPED_B import static org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent.EventType.SNAPSHOT_CREATED; import static org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent.EventType.SNAPSHOT_CREATING; import static org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent.EventType.SNAPSHOT_CREATION_ERROR; -import static org.eclipse.che.dto.server.DtoFactory.newDto; /** * Facade for Workspace related operations. @@ -449,10 +455,32 @@ public class WorkspaceManager { public void stopWorkspace(String workspaceId) throws ServerException, NotFoundException, ConflictException { + stopWorkspace(workspaceId, null); + } + + /** + * Asynchronously stops the workspace, + * creates a snapshot of it if {@code createSnapshot} is set to true. + * + * @param workspaceId + * the id of the workspace to stop + * @param createSnapshot + * true if create snapshot, false if don't, + * null if default behaviour should be used + * @throws ServerException + * when any server error occurs + * @throws NullPointerException + * when {@code workspaceId} is null + * @throws NotFoundException + * when workspace {@code workspaceId} doesn't have runtime + */ + public void stopWorkspace(String workspaceId, @Nullable Boolean createSnapshot) throws ConflictException, + NotFoundException, + ServerException { requireNonNull(workspaceId, "Required non-null workspace id"); final WorkspaceImpl workspace = normalizeState(workspaceDao.get(workspaceId)); checkWorkspaceIsRunning(workspace, "stop"); - performAsyncStop(workspace); + performAsyncStop(workspace, createSnapshot); } /** @@ -487,9 +515,9 @@ public class WorkspaceManager { final WorkspaceImpl workspace = normalizeState(workspaceDao.get(workspaceId)); checkWorkspaceIsRunning(workspace, "create a snapshot of"); executor.execute(ThreadLocalPropagateContext.wrap(() -> { - createSnapshotSync(workspace.getRuntime(), - workspace.getNamespace(), - workspaceId); + createSnapshotSync(workspace.getNamespace(), + workspaceId, + workspace.getRuntime().getActiveEnv()); })); } @@ -644,15 +672,20 @@ public class WorkspaceManager { * attribute set to true) and then stops the workspace(even if snapshot creation failed). */ @VisibleForTesting - void performAsyncStop(WorkspaceImpl workspace) throws ConflictException { + void performAsyncStop(WorkspaceImpl workspace, @Nullable Boolean createSnapshot) throws ConflictException { checkWorkspaceIsRunning(workspace, "stop"); - final String autoSnapshotAttr = workspace.getAttributes().get(AUTO_CREATE_SNAPSHOT); - boolean createSnapshot; + + final boolean snapshotBeforeStop; if (workspace.isTemporary()) { - createSnapshot = false; + snapshotBeforeStop = false; + } else if (createSnapshot != null) { + snapshotBeforeStop = createSnapshot; + } else if (workspace.getAttributes().containsKey(AUTO_CREATE_SNAPSHOT)) { + snapshotBeforeStop = parseBoolean(workspace.getAttributes().get(AUTO_CREATE_SNAPSHOT)); } else { - createSnapshot = autoSnapshotAttr == null ? defaultAutoSnapshot : parseBoolean(autoSnapshotAttr); + snapshotBeforeStop = defaultAutoSnapshot; } + executor.execute(ThreadLocalPropagateContext.wrap(() -> { final String stoppedBy = sessionUserNameOr(workspace.getAttributes().get(WORKSPACE_STOPPED_BY)); LOG.info("Workspace '{}:{}' with id '{}' is being stopped by user '{}'", @@ -660,7 +693,9 @@ public class WorkspaceManager { workspace.getConfig().getName(), workspace.getId(), firstNonNull(stoppedBy, "undefined")); - if (createSnapshot && !createSnapshotSync(workspace.getRuntime(), workspace.getNamespace(), workspace.getId())) { + if (snapshotBeforeStop && !createSnapshotSync(workspace.getNamespace(), + workspace.getId(), + workspace.getRuntime().getActiveEnv())) { LOG.warn("Could not create a snapshot of the workspace '{}:{}' with workspace id '{}'. The workspace will be stopped", workspace.getNamespace(), workspace.getConfig().getName(), @@ -702,81 +737,81 @@ public class WorkspaceManager { * otherwise returns false. */ @VisibleForTesting - boolean createSnapshotSync(WorkspaceRuntimeImpl runtime, String namespace, String workspaceId) { - eventService.publish(newDto(WorkspaceStatusEvent.class) - .withEventType(SNAPSHOT_CREATING) - .withWorkspaceId(workspaceId)); - String devMachineSnapshotFailMessage = null; - for (MachineImpl machine : runtime.getMachines()) { - String error = replaceSnapshot(machine, namespace); - if (error != null && machine.getConfig().isDev()) { - devMachineSnapshotFailMessage = error; - } - } - if (devMachineSnapshotFailMessage != null) { - eventService.publish(newDto(WorkspaceStatusEvent.class) - .withEventType(SNAPSHOT_CREATION_ERROR) - .withWorkspaceId(workspaceId) - .withError(devMachineSnapshotFailMessage)); - } else { - eventService.publish(newDto(WorkspaceStatusEvent.class) - .withEventType(SNAPSHOT_CREATED) - .withWorkspaceId(workspaceId)); - } - return devMachineSnapshotFailMessage == null; - } - - private String replaceSnapshot(MachineImpl machine, String namespace) { + boolean createSnapshotSync(String namespace, String workspaceId, String envName) { try { + runtimes.beginSnapshotting(workspaceId); + } catch (NotFoundException | ConflictException x) { + LOG.warn("Couldn't start snapshot creation of workspace '{}' due to error: '{}'", + workspaceId, + x.getMessage()); + return false; + } + + publishEvent(SNAPSHOT_CREATING, workspaceId); + + final List machines; + try { + machines = runtimes.get(workspaceId).getRuntime().getMachines(); + } catch (Exception x) { + throw new IllegalStateException(x.getLocalizedMessage(), x); + } + + LOG.info("Creating snapshot of workspace '{}', machines to snapshot: '{}'", workspaceId, machines.size()); + final List newSnapshots = new ArrayList<>(machines.size()); + Collections.sort(machines, comparing(m -> !m.getConfig().isDev(), Boolean::compare)); + for (MachineImpl machine : machines) { try { - SnapshotImpl oldSnapshot = snapshotDao.getSnapshot(machine.getWorkspaceId(), - machine.getEnvName(), - machine.getConfig().getName()); - snapshotDao.removeSnapshot(oldSnapshot.getId()); - - runtimes.removeSnapshot(oldSnapshot); - } catch (NotFoundException ignored) { - // Do nothing if no snapshot found + newSnapshots.add(runtimes.saveMachine(namespace, workspaceId, machine.getId())); + } catch (Exception x) { + if (machine.getConfig().isDev()) { + runtimes.finishSnapshotting(workspaceId); + publishEvent(SNAPSHOT_CREATION_ERROR, workspaceId, x.getMessage()); + return false; + } + LOG.warn(format("Couldn't create snapshot of machine '%s:%s:%s' in workspace '%s'", + namespace, + machine.getEnvName(), + machine.getConfig().getName(), + workspaceId)); } + } - SnapshotImpl snapshot = null; + try { + LOG.info("Saving new snapshots metadata, workspace id '{}'", workspaceId); + final List removed = snapshotDao.replaceSnapshots(workspaceId, envName, newSnapshots); + if (!removed.isEmpty()) { + LOG.info("Removing old snapshots, workspace id '{}', snapshots to remove '{}'", workspaceId, removed.size()); + removeSnapshotsBinaries(removed); + } + } catch (SnapshotException x) { + LOG.error(format("Couldn't remove existing snapshots metadata for workspace '%s'", workspaceId), x); + LOG.info("Removing newly created snapshots, workspace id '{}', snapshots to remove '{}'", workspaceId, newSnapshots.size()); + removeSnapshotsBinaries(newSnapshots); + runtimes.finishSnapshotting(workspaceId); + publishEvent(SNAPSHOT_CREATION_ERROR, workspaceId, x.getMessage()); + return false; + } + + runtimes.finishSnapshotting(workspaceId); + publishEvent(SNAPSHOT_CREATED, workspaceId); + + return true; + } + + private void removeSnapshotsBinaries(Collection snapshots) { + for (SnapshotImpl snapshot : snapshots) { try { - snapshot = runtimes.saveMachine(namespace, - machine.getWorkspaceId(), - machine.getId()); - // check if the workspace exists before creating a snapshot, - // if it is not an integrity constraint violation exception will occur, - // this may happen when workspace stop called simultaneously. - // The issue https://github.com/eclipse/che/issues/2683 should fix it - // in a way that it won't be possible to snapshot workspace simultaneously. - if (exists(machine.getWorkspaceId())) { - snapshotDao.saveSnapshot(snapshot); - } else { - LOG.warn("Snapshot for a workspace '{}' won't be saved, as the workspace doesn't exist anymore", - machine.getWorkspaceId()); - runtimes.removeSnapshot(snapshot); - } - } catch (ApiException e) { - if (snapshot != null) { - try { - runtimes.removeSnapshot(snapshot); - } catch (ApiException e1) { - LOG.error(format("Snapshot removal failed. Snapshot: %s. Error: %s", - snapshot, - e1.getLocalizedMessage()), - e1); - } - } - throw e; + runtimes.removeSnapshot(snapshot); + } catch (ServerException | NotFoundException x) { + LOG.error(format("Couldn't remove snapshot '%s', workspace id '%s'", + snapshot.getId(), + snapshot.getWorkspaceId()), + x); } - - return null; - } catch (ApiException apiEx) { - LOG.error("Snapshot creation failed. Error: " + apiEx.getLocalizedMessage(), apiEx); - return apiEx.getLocalizedMessage(); } } + @VisibleForTesting void checkWorkspaceIsRunning(WorkspaceImpl workspace, String operation) throws ConflictException { if (workspace.getStatus() != RUNNING) { @@ -808,6 +843,17 @@ public class WorkspaceManager { } } + private void publishEvent(EventType type, String workspaceId, String error) { + eventService.publish(DtoFactory.newDto(WorkspaceStatusEvent.class) + .withEventType(type) + .withWorkspaceId(workspaceId) + .withError(error)); + } + + private void publishEvent(EventType type, String workspaceId) { + publishEvent(type, workspaceId, null); + } + private WorkspaceImpl normalizeState(WorkspaceImpl workspace, RuntimeDescriptor descriptor) { if (descriptor != null) { workspace.setStatus(descriptor.getRuntimeStatus()); @@ -856,14 +902,4 @@ public class WorkspaceManager { final String namespace = nsPart.isEmpty() ? sessionUser().getUserName() : nsPart; return workspaceDao.get(wsName, namespace); } - - /** Returns true if workspace exists and false otherwise. */ - private boolean exists(String workspaceId) throws ServerException { - try { - workspaceDao.get(workspaceId); - } catch (NotFoundException x) { - return false; - } - return true; - } } diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java index 7425277f6d..a3c9b1570d 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java @@ -63,6 +63,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static java.lang.String.format; +import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.RUNNING; +import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.SNAPSHOTTING; import static org.eclipse.che.api.machine.shared.Constants.ENVIRONMENT_OUTPUT_CHANNEL_TEMPLATE; import static org.eclipse.che.dto.server.DtoFactory.newDto; import static org.slf4j.LoggerFactory.getLogger; @@ -401,10 +403,7 @@ public class WorkspaceRuntimes { NotFoundException { try (StripedLocks.ReadLock lock = stripedLocks.acquireReadLock(workspaceId)) { - WorkspaceState workspaceState = workspaces.get(workspaceId); - if (workspaceState == null || workspaceState.status != WorkspaceStatus.RUNNING) { - throw new ConflictException(format("Environment of workspace '%s' is not running", workspaceId)); - } + getRunningState(workspaceId); } List agents = Collections.singletonList("org.eclipse.che.terminal"); @@ -413,7 +412,7 @@ public class WorkspaceRuntimes { try (StripedLocks.WriteLock lock = stripedLocks.acquireWriteLock(workspaceId)) { WorkspaceState workspaceState = workspaces.get(workspaceId); - if (workspaceState == null || workspaceState.status != WorkspaceStatus.RUNNING) { + if (workspaceState == null || workspaceState.status != RUNNING) { try { environmentEngine.stopMachine(workspaceId, instance.getId()); } catch (NotFoundException | ServerException | ConflictException e) { @@ -426,6 +425,41 @@ public class WorkspaceRuntimes { return instance; } + /** + * Changes workspace runtimes status from RUNNING to SNAPSHOTTING. + * + * @param workspaceId + * the id of the workspace to begin snapshotting for + * @throws NotFoundException + * when workspace with such id doesn't have runtime + * @throws ConflictException + * when workspace status is different from SNAPSHOTTING + * @see WorkspaceStatus#SNAPSHOTTING + */ + public void beginSnapshotting(String workspaceId) throws NotFoundException, ConflictException { + try (StripedLocks.WriteLock ignored = stripedLocks.acquireWriteLock(workspaceId)) { + getRunningState(workspaceId).status = SNAPSHOTTING; + } + } + + /** + * Changes workspace runtimes status from SNAPSHOTTING back to RUNNING. + * This method won't affect workspace runtime or throw any exception + * if workspace is not in running status or doesn't have runtime. + * + * @param workspaceId + * the id of the workspace to finish snapshotting for + * @see WorkspaceStatus#SNAPSHOTTING + */ + public void finishSnapshotting(String workspaceId) { + try (StripedLocks.WriteLock ignored = stripedLocks.acquireWriteLock(workspaceId)) { + final WorkspaceState state = workspaces.get(workspaceId); + if (state != null && state.status == SNAPSHOTTING) { + state.status = RUNNING; + } + } + } + /** * Stops machine in a running environment. * @@ -447,7 +481,7 @@ public class WorkspaceRuntimes { ConflictException { try (StripedLocks.ReadLock lock = stripedLocks.acquireReadLock(workspaceId)) { WorkspaceState workspaceState = workspaces.get(workspaceId); - if (workspaceState == null || workspaceState.status != WorkspaceStatus.RUNNING) { + if (workspaceState == null || workspaceState.status != RUNNING) { throw new ConflictException(format("Environment of workspace '%s' is not running", workspaceId)); } } @@ -479,8 +513,8 @@ public class WorkspaceRuntimes { try (StripedLocks.ReadLock lock = stripedLocks.acquireReadLock(workspaceId)) { WorkspaceState workspaceState = workspaces.get(workspaceId); - if (workspaceState == null || workspaceState.status != WorkspaceStatus.RUNNING) { - throw new ConflictException(format("Environment of workspace '%s' is not running", workspaceId)); + if (workspaceState == null || !(workspaceState.status == SNAPSHOTTING || workspaceState.status == RUNNING)) { + throw new ConflictException(format("Environment of workspace '%s' is not running or snapshotting", workspaceId)); } } return environmentEngine.saveSnapshot(namespace, workspaceId, machineId); @@ -549,7 +583,7 @@ public class WorkspaceRuntimes { .build()); try (StripedLocks.WriteAllLock lock = stripedLocks.acquireWriteAllLock()) { for (Map.Entry workspace : workspaces.entrySet()) { - if (workspace.getValue().status.equals(WorkspaceStatus.RUNNING) || + if (workspace.getValue().status.equals(RUNNING) || workspace.getValue().status.equals(WorkspaceStatus.STARTING)) { stopEnvExecutor.execute(() -> { try { @@ -586,29 +620,30 @@ public class WorkspaceRuntimes { .withError(error)); } - private Instance getDevMachine(List machines) throws ServerException { - Optional devMachineOpt = machines.stream() - .filter(machine -> machine.getConfig().isDev()) - .findAny(); - - if (devMachineOpt.isPresent()) { - return devMachineOpt.get(); - } - throw new ServerException( - "Environment has booted but it doesn't contain dev machine. Environment has been stopped."); - } - private void ensurePreDestroyIsNotExecuted() throws ServerException { if (isPreDestroyInvoked) { throw new ServerException("Could not perform operation because application server is stopping"); } } + private WorkspaceState getRunningState(String workspaceId) throws NotFoundException, ConflictException { + final WorkspaceState state = workspaces.get(workspaceId); + if (state == null) { + throw new NotFoundException("Workspace with id '" + workspaceId + "' is not running"); + } + if (state.getStatus() != RUNNING) { + throw new ConflictException(format("Workspace with id '%s' is not in 'RUNNING', it's status is '%s'", + workspaceId, + state.getStatus())); + } + return state; + } + protected void launchAgents(Instance instance, List agents) throws ServerException { try { for (AgentKey agentKey : agentSorter.sort(agents)) { LOG.info("Launching '{}' agent", agentKey.getName()); - + Agent agent = agentRegistry.getAgent(agentKey); AgentLauncher launcher = launcherFactory.find(agentKey.getName(), instance.getConfig().getType()); launcher.launch(instance, agent); diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceService.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceService.java index bfa25feaa2..53fb01531e 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceService.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceService.java @@ -346,11 +346,13 @@ public class WorkspaceService extends Service { @ApiResponse(code = 404, message = "The workspace with specified id doesn't exist"), @ApiResponse(code = 403, message = "The user is not workspace owner"), @ApiResponse(code = 500, message = "Internal server error occurred")}) - public void stop(@ApiParam("The workspace id") @PathParam("id") String id) throws ForbiddenException, - NotFoundException, - ServerException, - ConflictException { - workspaceManager.stopWorkspace(id); + public void stop(@ApiParam("The workspace id") @PathParam("id") String id, + @ApiParam("Whether to snapshot workspace before stopping it") + @QueryParam("create-snapshot") Boolean createSnapshot) throws ForbiddenException, + NotFoundException, + ServerException, + ConflictException { + workspaceManager.stopWorkspace(id, createSnapshot); } @POST @@ -506,10 +508,10 @@ public class WorkspaceService extends Service { @ApiParam(value = "The name of the environment", required = true) @QueryParam("name") String envName) throws ServerException, - BadRequestException, - NotFoundException, - ConflictException, - ForbiddenException { + BadRequestException, + NotFoundException, + ConflictException, + ForbiddenException { requiredNotNull(newEnvironment, "New environment"); requiredNotNull(envName, "New environment name"); final WorkspaceImpl workspace = workspaceManager.getWorkspace(id); @@ -673,7 +675,7 @@ public class WorkspaceService extends Service { @ApiResponse(code = 404, message = "The workspace with specified id does not exist"), @ApiResponse(code = 500, message = "Internal server error occurred")}) public WsAgentHealthStateDto checkAgentHealth(@ApiParam(value = "Workspace id") - @PathParam("id") String key) throws NotFoundException, ServerException{ + @PathParam("id") String key) throws NotFoundException, ServerException { final WorkspaceImpl workspace = workspaceManager.getWorkspace(key); if (WorkspaceStatus.RUNNING != workspace.getStatus()) { return newDto(WsAgentHealthStateDto.class).withWorkspaceStatus(workspace.getStatus()); diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/WorkspaceConfigImpl.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/WorkspaceConfigImpl.java index fd2e2a08df..1ac45747c3 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/WorkspaceConfigImpl.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/WorkspaceConfigImpl.java @@ -204,13 +204,14 @@ public class WorkspaceConfigImpl implements WorkspaceConfig { @Override public String toString() { - return "UsersWorkspaceImpl{" + + return "WorkspaceConfigImpl{" + + "id=" + id + ", name='" + name + '\'' + + ", description='" + description + '\'' + ", defaultEnv='" + defaultEnv + '\'' + ", commands=" + commands + ", projects=" + projects + ", environments=" + environments + - ", description='" + description + '\'' + '}'; } diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java index aafdb818ce..0b70fe28fc 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java @@ -41,6 +41,7 @@ import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent; import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.commons.subject.Subject; import org.eclipse.che.commons.subject.SubjectImpl; +import org.eclipse.che.dto.server.DtoFactory; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InOrder; @@ -51,11 +52,14 @@ import org.testng.annotations.Listeners; import org.testng.annotations.Test; import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.UUID; import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.RUNNING; @@ -77,6 +81,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; @@ -99,23 +104,23 @@ public class WorkspaceManagerTest { private static final String NAMESPACE_2 = "userNS2"; @Mock - private EventService eventService; + private EventService eventService; @Mock - private WorkspaceDao workspaceDao; + private WorkspaceDao workspaceDao; @Mock - private WorkspaceValidator workspaceConfigValidator; + private WorkspaceValidator workspaceConfigValidator; @Mock - private MachineProcessManager client; + private MachineProcessManager client; @Mock - private WorkspaceRuntimes runtimes; + private WorkspaceRuntimes runtimes; @Mock - private AccountManager accountManager; + private AccountManager accountManager; @Mock - private SnapshotDao snapshotDao; - @Mock - private WorkspaceFilesCleaner workspaceFilesCleaner; + private SnapshotDao snapshotDao; @Captor - private ArgumentCaptor workspaceCaptor; + private ArgumentCaptor workspaceCaptor; + @Captor + private ArgumentCaptor> snapshotsCaptor; private WorkspaceManager workspaceManager; @@ -530,62 +535,105 @@ public class WorkspaceManagerTest { } @Test - public void shouldNotCreateSnapshotIfWorkspaceIsTemporaryAndAutoCreateSnapshotActivated() throws Exception { - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - workspace.getAttributes().put(Constants.AUTO_CREATE_SNAPSHOT, "true"); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - final RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - workspace.setTemporary(true); + public void createsSnapshotBeforeStoppingWorkspace() throws Exception { + final WorkspaceImpl workspace = createRunningWorkspace(); workspaceManager.stopWorkspace(workspace.getId()); - verify(workspaceManager, timeout(2000).never()).createSnapshotSync(anyObject(), anyString(), anyString()); - verify(runtimes, timeout(2000)).stop(workspace.getId()); + verify(runtimes, timeout(2000)).beginSnapshotting(workspace.getId()); + verify(runtimes, timeout(2000)).finishSnapshotting(workspace.getId()); + final Iterator machineIt = workspace.getRuntime().getMachines().iterator(); + verify(runtimes).saveMachine(workspace.getNamespace(), workspace.getId(), machineIt.next().getId()); + verify(runtimes).saveMachine(workspace.getNamespace(), workspace.getId(), machineIt.next().getId()); + verify(eventService).publish(DtoFactory.newDto(WorkspaceStatusEvent.class) + .withEventType(SNAPSHOT_CREATING) + .withWorkspaceId(workspace.getId())); + verify(eventService).publish(DtoFactory.newDto(WorkspaceStatusEvent.class) + .withEventType(SNAPSHOT_CREATED) + .withWorkspaceId(workspace.getId())); } @Test - public void shouldNotCreateSnapshotIfWorkspaceIsTemporaryAndAutoCreateSnapshotDisactivated() throws Exception { - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - workspace.getAttributes().put(Constants.AUTO_CREATE_SNAPSHOT, "false"); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - final RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - workspace.setTemporary(true); + public void failsToCreateSnapshotWhenDevMachineSnapshottingFailed() throws Exception { + final WorkspaceImpl workspace = createRunningWorkspace(); + when(runtimes.saveMachine(any(), any(), any())).thenThrow( new ServerException("test")); workspaceManager.stopWorkspace(workspace.getId()); - verify(workspaceManager, timeout(2000).never()).createSnapshotSync(anyObject(), anyString(), anyString()); - verify(runtimes, timeout(2000)).stop(workspace.getId()); + verify(runtimes, timeout(2000)).beginSnapshotting(workspace.getId()); + verify(runtimes, timeout(2000)).finishSnapshotting(workspace.getId()); + verify(eventService).publish(DtoFactory.newDto(WorkspaceStatusEvent.class) + .withEventType(SNAPSHOT_CREATING) + .withWorkspaceId(workspace.getId())); + verify(eventService).publish(DtoFactory.newDto(WorkspaceStatusEvent.class) + .withEventType(SNAPSHOT_CREATION_ERROR) + .withWorkspaceId(workspace.getId()) + .withError("test")); } @Test - public void shouldCreateWorkspaceSnapshotBeforeStoppingWorkspace() throws Exception { - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - workspace.getAttributes().put(Constants.AUTO_CREATE_SNAPSHOT, "true"); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - final RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); + public void removesNewlyCreatedSnapshotsWhenFailedToSaveTheirsMetadata() throws Exception { + final WorkspaceImpl workspace = createRunningWorkspace(); + when(snapshotDao.replaceSnapshots(eq(workspace.getId()), + eq(workspace.getRuntime().getActiveEnv()), + anyObject())).thenThrow(new SnapshotException("test")); workspaceManager.stopWorkspace(workspace.getId()); - verify(workspaceManager, timeout(2000)).createSnapshotSync(anyObject(), anyString(), anyString()); - verify(runtimes, timeout(2000)).stop(workspace.getId()); + verify(runtimes, timeout(2000)).beginSnapshotting(workspace.getId()); + verify(runtimes, timeout(2000)).finishSnapshotting(workspace.getId()); + verify(eventService).publish(DtoFactory.newDto(WorkspaceStatusEvent.class) + .withEventType(SNAPSHOT_CREATING) + .withWorkspaceId(workspace.getId())); + verify(eventService).publish(DtoFactory.newDto(WorkspaceStatusEvent.class) + .withEventType(SNAPSHOT_CREATION_ERROR) + .withWorkspaceId(workspace.getId()) + .withError("test")); + verify(snapshotDao).replaceSnapshots(eq(workspace.getId()), + eq(workspace.getRuntime().getActiveEnv()), + snapshotsCaptor.capture()); + final Iterator snapshotsIt = snapshotsCaptor.getValue().iterator(); + verify(runtimes).removeSnapshot(snapshotsIt.next()); + verify(runtimes).removeSnapshot(snapshotsIt.next()); + } + + @Test + public void removesOldSnapshotsWhenNewSnapshotsMetadataSuccessfullySaved() throws Exception { + final WorkspaceImpl workspace = createRunningWorkspace(); + final SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); + when(snapshotDao.replaceSnapshots(eq(workspace.getId()), + eq(workspace.getRuntime().getActiveEnv()), + anyObject())).thenReturn(singletonList(oldSnapshot)); + + workspaceManager.stopWorkspace(workspace.getId()); + + verify(runtimes, timeout(2000)).beginSnapshotting(workspace.getId()); + verify(runtimes, timeout(2000)).finishSnapshotting(workspace.getId()); + verify(eventService).publish(DtoFactory.newDto(WorkspaceStatusEvent.class) + .withEventType(SNAPSHOT_CREATING) + .withWorkspaceId(workspace.getId())); + verify(eventService).publish(DtoFactory.newDto(WorkspaceStatusEvent.class) + .withEventType(SNAPSHOT_CREATED) + .withWorkspaceId(workspace.getId())); + verify(runtimes).removeSnapshot(oldSnapshot); + } + + @Test + public void passedCreateSnapshotParameterIsUsedInPreferenceToAttribute() throws Exception { + final WorkspaceImpl workspace = createRunningWorkspace(); + + workspaceManager.stopWorkspace(workspace.getId(), false); + + verify(runtimes, never()).beginSnapshotting(workspace.getId()); + } + + @Test + public void passedNullCreateSnapshotParameterIsIgnored() throws Exception { + final WorkspaceImpl workspace = createRunningWorkspace(); + + workspaceManager.stopWorkspace(workspace.getId(), null); + + verify(runtimes, timeout(2000)).beginSnapshotting(workspace.getId()); } @Test(expectedExceptions = ConflictException.class, @@ -658,6 +706,46 @@ public class WorkspaceManagerTest { assertEquals(snapshots.get(0), wsSnapshot); } + @Test + public void shouldNotCreateSnapshotIfWorkspaceIsTemporaryAndAutoCreateSnapshotActivated() throws Exception { + final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); + workspace.getAttributes().put(Constants.AUTO_CREATE_SNAPSHOT, "true"); + when(workspaceDao.get(workspace.getId())).thenReturn(workspace); + final RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); + when(runtimes.get(any())).thenReturn(descriptor); + SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); + when(snapshotDao.getSnapshot(eq(workspace.getId()), + eq(workspace.getConfig().getDefaultEnv()), + anyString())) + .thenReturn(oldSnapshot); + workspace.setTemporary(true); + + workspaceManager.stopWorkspace(workspace.getId()); + + verify(workspaceManager, timeout(2000).never()).createSnapshotSync(anyObject(), anyString(), anyString()); + verify(runtimes, timeout(2000)).stop(workspace.getId()); + } + + @Test + public void shouldNotCreateSnapshotIfWorkspaceIsTemporaryAndAutoCreateSnapshotDisactivated() throws Exception { + final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); + workspace.getAttributes().put(Constants.AUTO_CREATE_SNAPSHOT, "false"); + when(workspaceDao.get(workspace.getId())).thenReturn(workspace); + final RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); + when(runtimes.get(any())).thenReturn(descriptor); + SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); + when(snapshotDao.getSnapshot(eq(workspace.getId()), + eq(workspace.getConfig().getDefaultEnv()), + anyString())) + .thenReturn(oldSnapshot); + workspace.setTemporary(true); + + workspaceManager.stopWorkspace(workspace.getId()); + + verify(workspaceManager, timeout(2000).never()).createSnapshotSync(anyObject(), anyString(), anyString()); + verify(runtimes, timeout(2000)).stop(workspace.getId()); + } + @Test public void shouldCreateWorkspaceSnapshotUsingDefaultValueForAutoRestore() throws Exception { // given @@ -682,7 +770,9 @@ public class WorkspaceManagerTest { workspaceManager.stopWorkspace(workspace.getId()); // then - verify(workspaceManager, timeout(2000)).createSnapshotSync(workspace.getRuntime(), workspace.getNamespace(), workspace.getId()); + verify(workspaceManager, timeout(2000)).createSnapshotSync(workspace.getNamespace(), + workspace.getId(), + workspace.getRuntime().getActiveEnv()); verify(runtimes, timeout(2000)).stop(any()); } @@ -838,9 +928,9 @@ public class WorkspaceManagerTest { workspaceManager.createSnapshot(workspace.getId()); // then - verify(workspaceManager, timeout(1_000)).createSnapshotSync(any(WorkspaceRuntimeImpl.class), - eq(workspace.getNamespace()), - eq(workspace.getId())); + verify(workspaceManager, timeout(1_000)).createSnapshotSync(eq(workspace.getNamespace()), + eq(workspace.getId()), + anyString()); } @Test(expectedExceptions = ConflictException.class, @@ -856,29 +946,6 @@ public class WorkspaceManagerTest { workspaceManager.createSnapshot(workspace.getId()); } - @Test - public void shouldSnapshotAllMachinesInWs() throws Exception { - // given - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - - // when - workspaceManager.createSnapshot(workspace.getId()); - - // then - verify(runtimes, timeout(1_000).times(2)).saveMachine(eq(workspace.getNamespace()), - eq(workspace.getId()), - anyString()); - verify(snapshotDao, timeout(1_000).times(2)).saveSnapshot(any(SnapshotImpl.class)); - } - @Test public void shouldSendEventOnStartSnapshotSaving() throws Exception { // given @@ -923,228 +990,6 @@ public class WorkspaceManagerTest { .withWorkspaceId(workspace.getId()))); } - @Test - public void shouldSendSnapshotSavingFailedEventIfDevMachineSnapshotSavingFailed() throws Exception { - // given - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - for (MachineImpl machine : descriptor.getRuntime().getMachines()) { - if (machine.getConfig().isDev()) { - when(runtimes.saveMachine(workspace.getNamespace(), workspace.getId(), machine.getId())) - .thenThrow(new ServerException("test error")); - } - } - - // when - workspaceManager.createSnapshot(workspace.getId()); - - // then - verify(eventService, timeout(1_000)).publish(eq(newDto(WorkspaceStatusEvent.class) - .withEventType(SNAPSHOT_CREATION_ERROR) - .withWorkspaceId(workspace.getId()) - .withError("test error"))); - } - - @Test - public void shouldNotSendSnapshotSavingFailedEventIfNonDevMachineSnapshotSavingFailed() throws Exception { - // given - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - for (MachineImpl machine : descriptor.getRuntime().getMachines()) { - if (!machine.getConfig().isDev()) { - when(runtimes.saveMachine(workspace.getNamespace(), workspace.getId(), machine.getId())) - .thenThrow(new ServerException("test error")); - } - } - - // when - workspaceManager.createSnapshot(workspace.getId()); - - // then - verify(eventService, timeout(1_000)).publish(eq(newDto(WorkspaceStatusEvent.class) - .withEventType(SNAPSHOT_CREATED) - .withWorkspaceId(workspace.getId()))); - } - - @Test - public void shouldReturnFalseOnFailureSnapshotSavingIfDevMachineSavingFailed() throws Exception { - // given - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - for (MachineImpl machine : descriptor.getRuntime().getMachines()) { - if (machine.getConfig().isDev()) { - when(runtimes.saveMachine(workspace.getNamespace(), workspace.getId(), machine.getId())) - .thenThrow(new ServerException("test error")); - } - } - - // when - boolean snapshotSavingStatus = workspaceManager.createSnapshotSync( - new WorkspaceRuntimeImpl(descriptor.getRuntime()), workspace.getNamespace(), workspace.getId()); - - // then - assertFalse(snapshotSavingStatus); - } - - @Test - public void shouldReturnTrueOnSuccessfulSavingSnapshotsForSeveralMachines() throws Exception { - // given - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - - // when - boolean snapshotSavingStatus = workspaceManager.createSnapshotSync( - new WorkspaceRuntimeImpl(descriptor.getRuntime()), workspace.getNamespace(), workspace.getId()); - - // then - assertTrue(snapshotSavingStatus); - // ensure that multiple machines were saved - verify(snapshotDao, timeout(1_000).atLeast(2)).saveSnapshot(any(SnapshotImpl.class)); - } - - @Test - public void shouldReturnTrueOnSavingSnapshotsForSeveralMachinesWhenNonDevMachineSavingFailed() throws Exception { - // given - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - for (MachineImpl machine : descriptor.getRuntime().getMachines()) { - if (!machine.getConfig().isDev()) { - when(runtimes.saveMachine(workspace.getNamespace(), workspace.getId(), machine.getId())) - .thenThrow(new ServerException("test error")); - } - } - - // when - boolean snapshotSavingStatus = workspaceManager.createSnapshotSync( - new WorkspaceRuntimeImpl(descriptor.getRuntime()), workspace.getNamespace(), workspace.getId()); - - // then - assertTrue(snapshotSavingStatus); - } - - @Test - public void shouldRemoveRuntimeSnapshotIfSavingSnapshotInDaoFails() throws Exception { - // given - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - doThrow(new SnapshotException("test error")).when(snapshotDao).saveSnapshot(any(SnapshotImpl.class)); - - // when - workspaceManager.createSnapshot(workspace.getId()); - - // then - verify(runtimes, timeout(1_000).times(2)).removeSnapshot(any(SnapshotImpl.class)); - } - - @Test - public void shouldIgnoreNotFoundExceptionOnOldSnapshotRemoval1() throws Exception { - // given - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - doThrow(new NotFoundException("test error")).when(snapshotDao).removeSnapshot(anyString()); - - // when - workspaceManager.createSnapshot(workspace.getId()); - - // then - verify(workspaceManager, timeout(1_000)).createSnapshotSync(any(WorkspaceRuntimeImpl.class), - eq(workspace.getNamespace()), - eq(workspace.getId())); - } - - @Test - public void shouldIgnoreNotFoundExceptionOnOldSnapshotRemoval2() throws Exception { - // given - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - SnapshotImpl oldSnapshot = mock(SnapshotImpl.class); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenReturn(oldSnapshot); - doThrow(new NotFoundException("test error")).when(runtimes).removeSnapshot(any(SnapshotImpl.class)); - - // when - workspaceManager.createSnapshot(workspace.getId()); - - // then - verify(workspaceManager, timeout(1_000)).createSnapshotSync(any(WorkspaceRuntimeImpl.class), - eq(workspace.getNamespace()), - eq(workspace.getId())); - } - - @Test - public void shouldIgnoreNotFoundExceptionOnOldSnapshotRemoval3() throws Exception { - // given - final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); - when(workspaceDao.get(workspace.getId())).thenReturn(workspace); - RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); - when(runtimes.get(any())).thenReturn(descriptor); - when(snapshotDao.getSnapshot(eq(workspace.getId()), - eq(workspace.getConfig().getDefaultEnv()), - anyString())) - .thenThrow(new NotFoundException("test error")); - - // when - workspaceManager.createSnapshot(workspace.getId()); - - // then - verify(workspaceManager, timeout(1_000)).createSnapshotSync(any(WorkspaceRuntimeImpl.class), - eq(workspace.getNamespace()), - eq(workspace.getId())); - } - @Test public void shouldBeAbleToStopMachine() throws Exception { // given @@ -1206,22 +1051,64 @@ public class WorkspaceManagerTest { verify(runtimes).getMachine(workspace.getId(), machine.getId()); } - private RuntimeDescriptor createDescriptor(WorkspaceImpl workspace, WorkspaceStatus status) { + private RuntimeDescriptor createDescriptor(WorkspaceImpl workspace, WorkspaceStatus status) + throws ServerException, NotFoundException, ConflictException { EnvironmentImpl environment = workspace.getConfig().getEnvironments().get(workspace.getConfig().getDefaultEnv()); assertNotNull(environment); final WorkspaceRuntimeImpl runtime = new WorkspaceRuntimeImpl(workspace.getConfig().getDefaultEnv()); - MachineImpl machine = spy(createMachine(workspace.getId(), workspace.getConfig().getDefaultEnv(), true)); - runtime.getMachines().add(machine); - MachineImpl machine2 = spy(createMachine(workspace.getId(), workspace.getConfig().getDefaultEnv(), false)); - runtime.getMachines().add(machine2); + final MachineImpl machine1 = spy(createMachine(workspace.getId(), workspace.getConfig().getDefaultEnv(), true)); + final MachineImpl machine2 = spy(createMachine(workspace.getId(), workspace.getConfig().getDefaultEnv(), false)); + final Map machines = new HashMap<>(); + machines.put(machine1.getId(), machine1); + machines.put(machine2.getId(), machine2); + runtime.getMachines().addAll(machines.values()); + runtime.setDevMachine(machine1); + + when(runtimes.saveMachine(any(), any(), anyObject())).thenAnswer(inv -> { + final String machineId = (String)inv.getArguments()[2]; + final MachineImpl machine = machines.get(machineId); + if (machine == null) { + return null; + } + return SnapshotImpl.builder() + .setWorkspaceId(machine.getWorkspaceId()) + .useCurrentCreationDate() + .generateId() + .setDescription("test") + .setDev(machine.getConfig().isDev()) + .setEnvName(machine.getEnvName()) + .setMachineName(machine.getConfig().getName()) + .build(); + }); final RuntimeDescriptor descriptor = mock(RuntimeDescriptor.class); when(descriptor.getRuntimeStatus()).thenReturn(status); when(descriptor.getRuntime()).thenReturn(runtime); + workspace.setRuntime(runtime); return descriptor; } + private WorkspaceImpl createRunningWorkspace() throws ServerException, NotFoundException, ConflictException { + // should be snapshotted when stopped + final WorkspaceImpl workspace = workspaceManager.createWorkspace(createConfig(), NAMESPACE); + workspace.getAttributes().put(Constants.AUTO_CREATE_SNAPSHOT, "true"); + when(workspaceDao.get(workspace.getId())).thenReturn(workspace); + + // has runtime + final RuntimeDescriptor descriptor = createDescriptor(workspace, RUNNING); + when(runtimes.get(workspace.getId())).thenReturn(descriptor); + + // doesn't have snapshots + when(snapshotDao.findSnapshots(workspace.getId())).thenReturn(emptyList()); + when(snapshotDao.replaceSnapshots(eq(workspace.getId()), + eq(workspace.getRuntime().getActiveEnv()), + any())).thenReturn(emptyList()); + + return workspace; + } + + private static WorkspaceConfigImpl createConfig() { EnvironmentImpl environment = new EnvironmentImpl(new EnvironmentRecipeImpl("type", "contentType", diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java index 2bcd4a9c87..170909a8eb 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java @@ -36,8 +36,11 @@ import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl; 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.commons.lang.NameGenerator; +import org.eclipse.che.dto.server.DtoFactory; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.BeforeMethod; @@ -51,23 +54,29 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.RUNNING; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; 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.assertNotNull; +import static org.testng.Assert.fail; /** * @author Yevhenii Voevodin @@ -436,8 +445,8 @@ public class WorkspaceRuntimesTest { verify(runtimes).launchAgents(instance, singletonList("org.eclipse.che.terminal")); } - @Test(expectedExceptions = ConflictException.class, - expectedExceptionsMessageRegExp = "Environment of workspace '.*' is not running") + @Test(expectedExceptions = NotFoundException.class, + expectedExceptionsMessageRegExp = "Workspace with id '.*' is not running") public void shouldNotStartMachineIfEnvironmentIsNotRunning() throws Exception { // when MachineConfigImpl config = createConfig(false); @@ -493,7 +502,7 @@ public class WorkspaceRuntimesTest { } @Test(expectedExceptions = ConflictException.class, - expectedExceptionsMessageRegExp = "Environment of workspace '.*' is not running") + expectedExceptionsMessageRegExp = "Environment of workspace 'workspaceId' is not running or snapshotting") public void shouldNotSaveMachineIfEnvironmentIsNotRunning() throws Exception { // when runtimes.saveMachine("namespace", "workspaceId", "machineId"); @@ -572,6 +581,72 @@ public class WorkspaceRuntimesTest { assertEquals(actualWorkspaces, expectedWorkspaces); } + @Test + public void changesStatusFromRunningToSnapshotting() throws Exception { + final WorkspaceImpl workspace = createWorkspace(); + runtimes.start(workspace, workspace.getConfig().getDefaultEnv(), false); + + runtimes.beginSnapshotting(workspace.getId()); + + assertEquals(runtimes.get(workspace.getId()).getRuntimeStatus(), WorkspaceStatus.SNAPSHOTTING); + } + + @Test + public void changesStatusFromSnapshottingToRunning() throws Exception { + final WorkspaceImpl workspace = createWorkspace(); + runtimes.start(workspace, workspace.getConfig().getDefaultEnv(), false); + runtimes.beginSnapshotting(workspace.getId()); + + runtimes.finishSnapshotting(workspace.getId()); + + assertEquals(runtimes.get(workspace.getId()).getRuntimeStatus(), WorkspaceStatus.RUNNING); + } + + @Test + public void doesNothingWhenWorkspaceDoesNotHaveRuntimeAndFinishSnapshottingIsCalled() throws Exception { + runtimes.finishSnapshotting("fake"); + } + + @Test + public void doesNothingWhenWorkspaceStatusIsNotSnapshottingAndFinishSnapshottingIsCalled() throws Exception { + final WorkspaceImpl workspace = createWorkspace(); + runtimes.start(workspace, workspace.getConfig().getDefaultEnv(), false); + + runtimes.finishSnapshotting(workspace.getId()); + + assertEquals(runtimes.get(workspace.getId()).getRuntimeStatus(), WorkspaceStatus.RUNNING); + } + + @Test(expectedExceptions = NotFoundException.class, + expectedExceptionsMessageRegExp = "Workspace with id 'non-existing' is not running") + public void throwsNotFoundExceptionWhenBeginningSnapshottingForNonExistingWorkspace() throws Exception { + runtimes.beginSnapshotting("non-existing"); + } + + @Test + public void throwsConflictExceptionWhenBeginningSnapshottingForNotRunningWorkspace() throws Exception { + final WorkspaceImpl workspace = createWorkspace(); + + doAnswer(inv -> { + // checking exception here + try { + runtimes.beginSnapshotting(workspace.getId()); + fail("Expected to get an exception"); + } catch (ConflictException x) { + assertEquals(x.getMessage(), format("Workspace with id '%s' is not in '%s', it's status is '%s'", + workspace.getId(), + WorkspaceStatus.RUNNING, + WorkspaceStatus.STARTING)); + } + return null; + }).when(eventService) + .publish(DtoFactory.newDto(WorkspaceStatusEvent.class) + .withEventType(EventType.STARTING) + .withWorkspaceId(workspace.getId())); + + runtimes.start(workspace, workspace.getConfig().getDefaultEnv(), false); + } + private static Instance createMachine(boolean isDev) { return createMachine(createConfig(isDev)); } diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceServiceTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceServiceTest.java index 0d8c27ae31..56c41eaa9f 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceServiceTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceServiceTest.java @@ -471,7 +471,7 @@ public class WorkspaceServiceTest { .delete(SECURE_PATH + "/workspace/" + workspace.getId() + "/runtime"); assertEquals(response.getStatusCode(), 204); - verify(wsManager).stopWorkspace(workspace.getId()); + verify(wsManager).stopWorkspace(workspace.getId(), null); } @Test diff --git a/wsmaster/wsmaster-local/pom.xml b/wsmaster/wsmaster-local/pom.xml index 1665ae9f3e..c52148a8cc 100644 --- a/wsmaster/wsmaster-local/pom.xml +++ b/wsmaster/wsmaster-local/pom.xml @@ -36,26 +36,10 @@ com.google.inject guice - - com.google.inject.extensions - guice-persist - - - com.jcraft - jsch - - - commons-fileupload - commons-fileupload - commons-io commons-io - - io.swagger - swagger-annotations - javax.annotation javax.annotation-api @@ -64,14 +48,6 @@ javax.inject javax.inject - - javax.validation - validation-api - - - javax.ws.rs - javax.ws.rs-api - org.eclipse.che.core che-core-api-account @@ -80,22 +56,10 @@ org.eclipse.che.core che-core-api-core - - org.eclipse.che.core - che-core-api-dto - - - org.eclipse.che.core - che-core-api-jdbc - org.eclipse.che.core che-core-api-machine - - org.eclipse.che.core - che-core-api-machine-shared - org.eclipse.che.core che-core-api-model @@ -104,18 +68,10 @@ org.eclipse.che.core che-core-api-ssh - - org.eclipse.che.core - che-core-api-ssh-shared - org.eclipse.che.core che-core-api-user - - org.eclipse.che.core - che-core-api-user-shared - org.eclipse.che.core che-core-api-workspace @@ -128,22 +84,10 @@ org.eclipse.che.core che-core-commons-lang - - org.eclipse.persistence - eclipselink - - - org.eclipse.persistence - javax.persistence - org.everrest everrest-core - - org.everrest - everrest-websockets - org.slf4j slf4j-api @@ -218,26 +162,4 @@ test - - - - org.apache.maven.plugins - maven-dependency-plugin - - - - unpack-dependencies - - - ${project.build.testOutputDirectory} - che-core-api-user, - che-core-api-machine, - che-core-api-ssh - test - - - - - - diff --git a/wsmaster/wsmaster-local/src/main/java/org/eclipse/che/api/local/LocalSnapshotDaoImpl.java b/wsmaster/wsmaster-local/src/main/java/org/eclipse/che/api/local/LocalSnapshotDaoImpl.java index 925763f929..951128aa5c 100644 --- a/wsmaster/wsmaster-local/src/main/java/org/eclipse/che/api/local/LocalSnapshotDaoImpl.java +++ b/wsmaster/wsmaster-local/src/main/java/org/eclipse/che/api/local/LocalSnapshotDaoImpl.java @@ -27,6 +27,7 @@ import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -119,6 +120,12 @@ public class LocalSnapshotDaoImpl implements SnapshotDao { snapshots.remove(snapshotId); } + @Override + public List replaceSnapshots(String workspaceId, String envName, Collection newSnapshots) + throws SnapshotException { + throw new RuntimeException("Not implemented"); + } + @PostConstruct public synchronized void loadSnapshots() { snapshots.putAll(snapshotStorage.loadMap(new TypeToken>() {}));