diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties
index 04ebcda8b4..fc0339b92b 100644
--- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties
+++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties
@@ -668,3 +668,10 @@ che.workspace.devfile.async.storage.plugin=eclipse/che-async-pv-plugin/nightly
# Docker image for the Che async storage
che.infra.kubernetes.async.storage.image=quay.io/eclipse/che-workspace-data-sync-storage:latest
+
+# The timeout for the Asynchronous Storage Pod shutdown after stopping the last used workspace.
+# Value less or equal to 0 interpreted as disabling shutdown ability.
+che.infra.kubernetes.async.storage.shutdown_timeout_min=120
+
+# Defines the period with which the Asynchronous Storage Pod stopping ability will be performed (once in 30 minutes by default)
+che.infra.kubernetes.async.storage.shutdown_check_period_min=30
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/AsyncStoragePodWatcher.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/AsyncStoragePodWatcher.java
new file mode 100644
index 0000000000..260c2ce438
--- /dev/null
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/AsyncStoragePodWatcher.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2012-2018 Red Hat, Inc.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+
+package org.eclipse.che.workspace.infrastructure.kubernetes.provision;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.lang.Long.parseLong;
+import static java.time.Instant.now;
+import static java.time.Instant.ofEpochSecond;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.eclipse.che.api.core.Pages.iterateLazily;
+import static org.eclipse.che.api.workspace.shared.Constants.LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE;
+import static org.eclipse.che.api.workspace.shared.Constants.LAST_ACTIVITY_TIME;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.CommonPVCStrategy.COMMON_STRATEGY;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.AsyncStorageProvisioner.ASYNC_STORAGE;
+
+import io.fabric8.kubernetes.api.model.DoneablePod;
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.client.dsl.PodResource;
+import java.time.Instant;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.model.user.User;
+import org.eclipse.che.api.user.server.PreferenceManager;
+import org.eclipse.che.api.user.server.UserManager;
+import org.eclipse.che.api.workspace.server.WorkspaceRuntimes;
+import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
+import org.eclipse.che.commons.annotation.Nullable;
+import org.eclipse.che.commons.schedule.ScheduleDelay;
+import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Periodically checks ability to stop Asynchronous Storage Pod. It will periodically revise
+ * UserPreferences of all registered user and check specialized preferences. Preferences should be
+ * recorded if last workspace stopped and cleanup on start any workspace. Required preferences to
+ * initiate stop procedure for Asynchronous Storage Pod : {@link
+ * org.eclipse.che.api.workspace.shared.Constants#LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE} : should
+ * contain last used infrastructure namespace {@link
+ * org.eclipse.che.api.workspace.shared.Constants#LAST_ACTIVITY_TIME} : seconds then workspace
+ * stopped in the Java epoch time format (aka Unix time)
+ */
+@Singleton
+public class AsyncStoragePodWatcher {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AsyncStoragePodWatcher.class);
+
+ private final KubernetesClientFactory kubernetesClientFactory;
+ private final UserManager userManager;
+ private final PreferenceManager preferenceManager;
+ private final WorkspaceRuntimes runtimes;
+ private final long shutdownTimeoutSec;
+ private final boolean isAsyncStoragePodCanBeRun;
+
+ @Inject
+ public AsyncStoragePodWatcher(
+ KubernetesClientFactory kubernetesClientFactory,
+ UserManager userManager,
+ PreferenceManager preferenceManager,
+ WorkspaceRuntimes runtimes,
+ @Named("che.infra.kubernetes.async.storage.shutdown_timeout_min") long shutdownTimeoutMin,
+ @Named("che.infra.kubernetes.pvc.strategy") String pvcStrategy,
+ @Named("che.infra.kubernetes.namespace.allow_user_defined")
+ boolean allowUserDefinedNamespaces,
+ @Nullable @Named("che.infra.kubernetes.namespace.default") String defaultNamespaceName,
+ @Named("che.limits.user.workspaces.run.count") int runtimesPerUser) {
+ this.kubernetesClientFactory = kubernetesClientFactory;
+ this.userManager = userManager;
+ this.preferenceManager = preferenceManager;
+ this.runtimes = runtimes;
+ this.shutdownTimeoutSec = MINUTES.toSeconds(shutdownTimeoutMin);
+
+ isAsyncStoragePodCanBeRun =
+ isAsyncStoragePodCanBeRun(
+ pvcStrategy, allowUserDefinedNamespaces, defaultNamespaceName, runtimesPerUser);
+ }
+
+ /**
+ * Checking current system configuration on ability to run Async Storage Pod. Will be checked next
+ * value of properties:
+ *
+ *
+ * - che.infra.kubernetes.namespace.default=-che
+ *
- che.infra.kubernetes.namespace.allow_user_defined=false
+ *
- che.infra.kubernetes.pvc.strategy=common
+ *
- che.limits.user.workspaces.run.count=1
+ *
+ */
+ private boolean isAsyncStoragePodCanBeRun(
+ String pvcStrategy,
+ boolean allowUserDefinedNamespaces,
+ String defaultNamespaceName,
+ int runtimesPerUser) {
+ return !allowUserDefinedNamespaces
+ && COMMON_STRATEGY.equals(pvcStrategy)
+ && runtimesPerUser == 1
+ && !isNullOrEmpty(defaultNamespaceName)
+ && defaultNamespaceName.contains("");
+ }
+
+ @ScheduleDelay(
+ unit = MINUTES,
+ initialDelay = 1,
+ delayParameterName = "che.infra.kubernetes.async.storage.shutdown_check_period_min")
+ public void check() {
+ if (isAsyncStoragePodCanBeRun
+ && shutdownTimeoutSec
+ > 0) { // if system not support async storage mode or idling time set to 0 or less
+ // do nothing
+ for (User user :
+ iterateLazily((maxItems, skipCount) -> userManager.getAll(maxItems, skipCount))) {
+ try {
+ String owner = user.getId();
+ Map preferences = preferenceManager.find(owner);
+ String lastTimeAccess = preferences.get(LAST_ACTIVITY_TIME);
+ String namespace = preferences.get(LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE);
+
+ if (isNullOrEmpty(namespace)
+ || isNullOrEmpty(lastTimeAccess)
+ || !runtimes.getInProgress(owner).isEmpty()) {
+ continue;
+ }
+ long lastTimeAccessSec = parseLong(lastTimeAccess);
+ Instant expectedShutdownAfter =
+ ofEpochSecond(lastTimeAccessSec).plusSeconds(shutdownTimeoutSec);
+ if (now().isAfter(expectedShutdownAfter)) {
+ PodResource doneablePodResource =
+ kubernetesClientFactory
+ .create()
+ .pods()
+ .inNamespace(namespace)
+ .withName(ASYNC_STORAGE);
+ if (doneablePodResource.get() != null) {
+ doneablePodResource.delete();
+ }
+ }
+ } catch (InfrastructureException | ServerException e) {
+ LOG.error(e.getMessage(), e);
+ }
+ }
+ }
+ }
+}
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/AsyncStoragePodWatcherTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/AsyncStoragePodWatcherTest.java
new file mode 100644
index 0000000000..3c90e84fb1
--- /dev/null
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/AsyncStoragePodWatcherTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (c) 2012-2018 Red Hat, Inc.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che.workspace.infrastructure.kubernetes.provision;
+
+import static java.time.Instant.now;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singleton;
+import static java.util.UUID.randomUUID;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.CommonPVCStrategy.COMMON_STRATEGY;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.AsyncStorageProvisioner.ASYNC_STORAGE;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import io.fabric8.kubernetes.api.model.DoneablePod;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.dsl.MixedOperation;
+import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
+import io.fabric8.kubernetes.client.dsl.PodResource;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.user.server.PreferenceManager;
+import org.eclipse.che.api.user.server.UserManager;
+import org.eclipse.che.api.user.server.model.impl.UserImpl;
+import org.eclipse.che.api.workspace.server.WorkspaceRuntimes;
+import org.eclipse.che.api.workspace.server.spi.InternalRuntime;
+import org.eclipse.che.api.workspace.shared.Constants;
+import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+@Listeners(value = {MockitoTestNGListener.class})
+public class AsyncStoragePodWatcherTest {
+
+ private final String NAMESPACE = randomUUID().toString();
+ private final String WORKSPACE_ID = randomUUID().toString();
+ private final String USER_ID = randomUUID().toString();
+
+ private Map userPref;
+
+ @Mock private KubernetesClientFactory kubernetesClientFactory;
+ @Mock private UserManager userManager;
+ @Mock private PreferenceManager preferenceManager;
+ @Mock private WorkspaceRuntimes runtimes;
+ @Mock private KubernetesClient kubernetesClient;
+ @Mock private PodResource podResource;
+ @Mock private MixedOperation mixedOperationPod;
+ @Mock private NonNamespaceOperation namespacePodOperation;
+ @Mock private UserImpl user;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ when(user.getId()).thenReturn(USER_ID);
+ userPref = new HashMap<>(3);
+ long epochSecond = now().getEpochSecond();
+ long activityTime = epochSecond - 600; // stored time 10 minutes early
+ userPref.put(Constants.LAST_ACTIVITY_TIME, Long.toString(activityTime));
+ userPref.put(Constants.LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE, NAMESPACE);
+ when(preferenceManager.find(USER_ID)).thenReturn(userPref);
+
+ Page userPage = new Page<>(Collections.singleton(user), 0, 1, 1);
+ when(userManager.getAll(anyInt(), anyLong())).thenReturn(userPage);
+
+ when(kubernetesClientFactory.create()).thenReturn(kubernetesClient);
+ when(kubernetesClient.pods()).thenReturn(mixedOperationPod);
+ when(mixedOperationPod.inNamespace(NAMESPACE)).thenReturn(namespacePodOperation);
+ when(namespacePodOperation.withName(ASYNC_STORAGE)).thenReturn(podResource);
+ }
+
+ @Test
+ public void shouldDeleteAsyncStoragePod() throws Exception {
+ AsyncStoragePodWatcher watcher =
+ new AsyncStoragePodWatcher(
+ kubernetesClientFactory,
+ userManager,
+ preferenceManager,
+ runtimes,
+ 1,
+ COMMON_STRATEGY,
+ false,
+ "",
+ 1);
+
+ when(runtimes.getInProgress(USER_ID)).thenReturn(emptySet());
+
+ ObjectMeta meta = new ObjectMeta();
+ meta.setName(ASYNC_STORAGE);
+ Pod pod = new Pod();
+ pod.setMetadata(meta);
+ when(podResource.get()).thenReturn(pod);
+
+ watcher.check();
+
+ verify(preferenceManager).find(USER_ID);
+ verify(podResource).delete();
+ }
+
+ @Test
+ public void shouldNotDeleteAsyncStoragePodIfTooEarly() throws Exception {
+ AsyncStoragePodWatcher watcher =
+ new AsyncStoragePodWatcher(
+ kubernetesClientFactory,
+ userManager,
+ preferenceManager,
+ runtimes,
+ 10,
+ COMMON_STRATEGY,
+ false,
+ "",
+ 1);
+ long epochSecond = now().getEpochSecond();
+ userPref.put(Constants.LAST_ACTIVITY_TIME, Long.toString(epochSecond));
+
+ watcher.check();
+
+ verify(preferenceManager).find(USER_ID);
+ verifyNoMoreInteractions(kubernetesClientFactory);
+ verifyNoMoreInteractions(podResource);
+ }
+
+ @Test
+ public void shouldNotDeleteAsyncStoragePodIfHasActiveRuntime() throws Exception {
+ AsyncStoragePodWatcher watcher =
+ new AsyncStoragePodWatcher(
+ kubernetesClientFactory,
+ userManager,
+ preferenceManager,
+ runtimes,
+ 1,
+ COMMON_STRATEGY,
+ false,
+ "",
+ 1);
+
+ // has active runtime
+ InternalRuntime runtime = mock(InternalRuntime.class);
+ when(runtime.getOwner()).thenReturn(USER_ID);
+ when(runtimes.getInProgress(USER_ID)).thenReturn(singleton(WORKSPACE_ID));
+ when(runtimes.getInternalRuntime(WORKSPACE_ID)).thenReturn(runtime);
+
+ Page userPage = new Page<>(Collections.singleton(user), 0, 1, 1);
+ when(userManager.getAll(anyInt(), anyLong())).thenReturn(userPage);
+
+ watcher.check();
+
+ verify(preferenceManager).find(USER_ID);
+ verifyNoMoreInteractions(kubernetesClientFactory);
+ verifyNoMoreInteractions(podResource);
+ }
+
+ @Test
+ public void shouldNotDeleteAsyncStoragePodIfNoRecord() throws Exception {
+ AsyncStoragePodWatcher watcher =
+ new AsyncStoragePodWatcher(
+ kubernetesClientFactory,
+ userManager,
+ preferenceManager,
+ runtimes,
+ 1,
+ COMMON_STRATEGY,
+ false,
+ "",
+ 1);
+ when(preferenceManager.find(USER_ID)).thenReturn(emptyMap()); // no records in user preferences
+
+ watcher.check();
+
+ verify(preferenceManager).find(USER_ID);
+ verifyNoMoreInteractions(kubernetesClientFactory);
+ verifyNoMoreInteractions(podResource);
+ }
+
+ @Test
+ public void shouldDoNothingIfNotCommonPvcStrategy() throws Exception {
+ AsyncStoragePodWatcher watcher =
+ new AsyncStoragePodWatcher(
+ kubernetesClientFactory,
+ userManager,
+ preferenceManager,
+ runtimes,
+ 1,
+ "my-own-strategy",
+ false,
+ "",
+ 1);
+ when(preferenceManager.find(USER_ID)).thenReturn(emptyMap()); // no records in user preferences
+
+ watcher.check();
+
+ verifyNoMoreInteractions(preferenceManager);
+ verifyNoMoreInteractions(kubernetesClientFactory);
+ verifyNoMoreInteractions(podResource);
+ }
+
+ @Test
+ public void shouldDoNothingIfAllowedUserDefinedNamespaces() throws Exception {
+ AsyncStoragePodWatcher watcher =
+ new AsyncStoragePodWatcher(
+ kubernetesClientFactory,
+ userManager,
+ preferenceManager,
+ runtimes,
+ 1,
+ "my-own-strategy",
+ true,
+ "",
+ 1);
+ when(preferenceManager.find(USER_ID)).thenReturn(emptyMap()); // no records in user preferences
+
+ watcher.check();
+
+ verifyNoMoreInteractions(preferenceManager);
+ verifyNoMoreInteractions(kubernetesClientFactory);
+ verifyNoMoreInteractions(podResource);
+ }
+
+ @Test
+ public void shouldDoNothingIfDefaultNamespaceNotCorrect() throws Exception {
+ AsyncStoragePodWatcher watcher =
+ new AsyncStoragePodWatcher(
+ kubernetesClientFactory,
+ userManager,
+ preferenceManager,
+ runtimes,
+ 1,
+ "my-own-strategy",
+ true,
+ "",
+ 1);
+ when(preferenceManager.find(USER_ID)).thenReturn(emptyMap()); // no records in user preferences
+
+ watcher.check();
+
+ verifyNoMoreInteractions(preferenceManager);
+ verifyNoMoreInteractions(kubernetesClientFactory);
+ verifyNoMoreInteractions(podResource);
+ }
+
+ @Test
+ public void shouldDoNothingIfAllowMoreThanOneRuntime() throws Exception {
+ AsyncStoragePodWatcher watcher =
+ new AsyncStoragePodWatcher(
+ kubernetesClientFactory,
+ userManager,
+ preferenceManager,
+ runtimes,
+ 1,
+ "my-own-strategy",
+ true,
+ "",
+ 2);
+ when(preferenceManager.find(USER_ID)).thenReturn(emptyMap()); // no records in user preferences
+
+ watcher.check();
+
+ verifyNoMoreInteractions(preferenceManager);
+ verifyNoMoreInteractions(kubernetesClientFactory);
+ verifyNoMoreInteractions(podResource);
+ }
+
+ @Test
+ public void shouldDoNothingIfShutdownTimeSetToZero() throws Exception {
+ AsyncStoragePodWatcher watcher =
+ new AsyncStoragePodWatcher(
+ kubernetesClientFactory,
+ userManager,
+ preferenceManager,
+ runtimes,
+ 0,
+ COMMON_STRATEGY,
+ false,
+ "",
+ 1);
+ when(preferenceManager.find(USER_ID)).thenReturn(emptyMap()); // no records in user preferences
+
+ watcher.check();
+
+ verifyNoMoreInteractions(preferenceManager);
+ verifyNoMoreInteractions(kubernetesClientFactory);
+ verifyNoMoreInteractions(podResource);
+ }
+}
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java
index 9c167bf8c4..ec7110b4e0 100644
--- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java
+++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java
@@ -58,6 +58,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.Workspa
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumeStrategyProvider;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy;
import org.eclipse.che.workspace.infrastructure.kubernetes.provision.AsyncStoragePodInterceptor;
+import org.eclipse.che.workspace.infrastructure.kubernetes.provision.AsyncStoragePodWatcher;
import org.eclipse.che.workspace.infrastructure.kubernetes.provision.AsyncStorageProvisioner;
import org.eclipse.che.workspace.infrastructure.kubernetes.provision.GatewayTlsProvisioner;
import org.eclipse.che.workspace.infrastructure.kubernetes.provision.KubernetesCheApiExternalEnvVarProvider;
@@ -254,5 +255,6 @@ public class OpenShiftInfraModule extends AbstractModule {
bind(NonTlsDistributedClusterModeNotifier.class);
bind(AsyncStorageProvisioner.class);
bind(AsyncStoragePodInterceptor.class);
+ bind(AsyncStoragePodWatcher.class);
}
}
diff --git a/multiuser/api/che-multiuser-api-resource/pom.xml b/multiuser/api/che-multiuser-api-resource/pom.xml
index 8e0dfb8608..d5943af8e8 100644
--- a/multiuser/api/che-multiuser-api-resource/pom.xml
+++ b/multiuser/api/che-multiuser-api-resource/pom.xml
@@ -74,6 +74,10 @@
org.eclipse.che.core
che-core-api-model
+
+ org.eclipse.che.core
+ che-core-api-user
+
org.eclipse.che.core
che-core-api-workspace
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/workspace/LimitsCheckingWorkspaceManager.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/workspace/LimitsCheckingWorkspaceManager.java
index 031b75d623..77f169e8ca 100644
--- a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/workspace/LimitsCheckingWorkspaceManager.java
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/workspace/LimitsCheckingWorkspaceManager.java
@@ -32,6 +32,7 @@ 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.config.Environment;
import org.eclipse.che.api.core.notification.EventService;
+import org.eclipse.che.api.user.server.PreferenceManager;
import org.eclipse.che.api.workspace.server.WorkspaceManager;
import org.eclipse.che.api.workspace.server.WorkspaceRuntimes;
import org.eclipse.che.api.workspace.server.WorkspaceValidator;
@@ -78,6 +79,7 @@ public class LimitsCheckingWorkspaceManager extends WorkspaceManager {
WorkspaceRuntimes runtimes,
EventService eventService,
AccountManager accountManager,
+ PreferenceManager preferenceManager,
WorkspaceValidator workspaceValidator,
// own injects
@Named("che.limits.workspace.env.ram") String maxRamPerEnv,
@@ -90,6 +92,7 @@ public class LimitsCheckingWorkspaceManager extends WorkspaceManager {
runtimes,
eventService,
accountManager,
+ preferenceManager,
workspaceValidator,
devfileIntegrityValidator);
this.environmentRamCalculator = environmentRamCalculator;
diff --git a/multiuser/api/che-multiuser-api-resource/src/test/java/org/eclipse/che/multiuser/resource/api/workspace/LimitsCheckingWorkspaceManagerTest.java b/multiuser/api/che-multiuser-api-resource/src/test/java/org/eclipse/che/multiuser/resource/api/workspace/LimitsCheckingWorkspaceManagerTest.java
index 714cd45fb7..5557c40568 100644
--- a/multiuser/api/che-multiuser-api-resource/src/test/java/org/eclipse/che/multiuser/resource/api/workspace/LimitsCheckingWorkspaceManagerTest.java
+++ b/multiuser/api/che-multiuser-api-resource/src/test/java/org/eclipse/che/multiuser/resource/api/workspace/LimitsCheckingWorkspaceManagerTest.java
@@ -266,6 +266,7 @@ public class LimitsCheckingWorkspaceManagerTest {
null,
null,
null,
+ null,
maxRamPerEnv,
environmentRamCalculator,
resourceManager,
diff --git a/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/Constants.java b/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/Constants.java
index fffa9afc36..3db754d546 100644
--- a/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/Constants.java
+++ b/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/Constants.java
@@ -216,5 +216,15 @@ public final class Constants {
public static final String WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE =
"infrastructureNamespace";
+ /**
+ * The attribute for storing the infrastructure namespace of last used workspace, it recorded on
+ * workspace stop if no more running
+ */
+ public static final String LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE =
+ "lastUsedInfrastructureNamespace";
+
+ /** The attribute for storing the time then last active workspace was stopped */
+ public static final String LAST_ACTIVITY_TIME = "lastActivityTime";
+
private Constants() {}
}
diff --git a/wsmaster/che-core-api-workspace/pom.xml b/wsmaster/che-core-api-workspace/pom.xml
index fc2ced6aae..755fe501b9 100644
--- a/wsmaster/che-core-api-workspace/pom.xml
+++ b/wsmaster/che-core-api-workspace/pom.xml
@@ -102,6 +102,10 @@
org.eclipse.che.core
che-core-api-system-shared
+
+ org.eclipse.che.core
+ che-core-api-user
+
org.eclipse.che.core
che-core-api-workspace-shared
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 af0e5dff50..80e158e94c 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
@@ -15,12 +15,15 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
+import static java.time.Instant.now;
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.STARTING;
import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STOPPED;
import static org.eclipse.che.api.workspace.shared.Constants.CREATED_ATTRIBUTE_NAME;
import static org.eclipse.che.api.workspace.shared.Constants.ERROR_MESSAGE_ATTRIBUTE_NAME;
+import static org.eclipse.che.api.workspace.shared.Constants.LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE;
+import static org.eclipse.che.api.workspace.shared.Constants.LAST_ACTIVITY_TIME;
import static org.eclipse.che.api.workspace.shared.Constants.STOPPED_ABNORMALLY_ATTRIBUTE_NAME;
import static org.eclipse.che.api.workspace.shared.Constants.STOPPED_ATTRIBUTE_NAME;
import static org.eclipse.che.api.workspace.shared.Constants.UPDATED_ATTRIBUTE_NAME;
@@ -28,6 +31,7 @@ import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_GENERATE_
import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE;
import com.google.inject.Inject;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
@@ -45,6 +49,7 @@ import org.eclipse.che.api.core.model.workspace.WorkspaceConfig;
import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
import org.eclipse.che.api.core.model.workspace.devfile.Devfile;
import org.eclipse.che.api.core.notification.EventService;
+import org.eclipse.che.api.user.server.PreferenceManager;
import org.eclipse.che.api.workspace.server.devfile.FileContentProvider;
import org.eclipse.che.api.workspace.server.devfile.exception.DevfileFormatException;
import org.eclipse.che.api.workspace.server.devfile.validator.DevfileIntegrityValidator;
@@ -83,6 +88,7 @@ public class WorkspaceManager {
private final WorkspaceRuntimes runtimes;
private final AccountManager accountManager;
private final EventService eventService;
+ private final PreferenceManager preferenceManager;
private final WorkspaceValidator validator;
private final DevfileIntegrityValidator devfileIntegrityValidator;
@@ -92,12 +98,14 @@ public class WorkspaceManager {
WorkspaceRuntimes runtimes,
EventService eventService,
AccountManager accountManager,
+ PreferenceManager preferenceManager,
WorkspaceValidator validator,
DevfileIntegrityValidator devfileIntegrityValidator) {
this.workspaceDao = workspaceDao;
this.runtimes = runtimes;
this.accountManager = accountManager;
this.eventService = eventService;
+ this.preferenceManager = preferenceManager;
this.validator = validator;
this.devfileIntegrityValidator = devfileIntegrityValidator;
}
@@ -416,6 +424,8 @@ public class WorkspaceManager {
workspace.getAttributes().put(STOPPED_ABNORMALLY_ATTRIBUTE_NAME, Boolean.toString(false));
workspaceDao.update(workspace);
}
+ String namespace = workspace.getAttributes().get(WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE);
+ final String owner = workspace.getRuntime().getOwner();
runtimes
.stopAsync(workspace, options)
@@ -424,9 +434,37 @@ public class WorkspaceManager {
if (workspace.isTemporary()) {
removeWorkspaceQuietly(workspace.getId());
}
+ try {
+ if (runtimes.getActive(owner).isEmpty()) {
+ recordLastWorkspaceStoppedTime(namespace, owner);
+ }
+ } catch (ServerException | InfrastructureException e) {
+ LOG.error(e.getMessage(), e);
+ }
});
}
+ private void recordLastWorkspaceStoppedTime(String namespace, String owner) {
+ try {
+ Map preferences = preferenceManager.find(owner);
+ String currentTime = Long.toString(now().getEpochSecond());
+ preferences.put(LAST_ACTIVITY_TIME, currentTime);
+ preferences.put(LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE, namespace);
+ preferenceManager.update(owner, preferences);
+ } catch (ServerException e) {
+ LOG.error(e.getMessage(), e);
+ }
+ }
+
+ private void cleanLastWorkspaceStoppedTime(String owner) {
+ try {
+ preferenceManager.remove(
+ owner, Arrays.asList(LAST_ACTIVITY_TIME, LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE));
+ } catch (ServerException e) {
+ LOG.error(e.getMessage(), e);
+ }
+ }
+
/** Returns a set of supported recipe types */
public Set getSupportedRecipes() {
return runtimes.getSupportedRecipes();
@@ -577,6 +615,8 @@ public class WorkspaceManager {
workspace.getAttributes().remove(ERROR_MESSAGE_ATTRIBUTE_NAME);
workspaceDao.update(workspace);
+
+ cleanLastWorkspaceStoppedTime(workspace.getRuntime().getOwner());
} catch (NotFoundException | ServerException | ConflictException e) {
LOG.warn(
String.format(
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 f7ee5e8c66..0b4ae11e94 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
@@ -37,6 +37,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -583,6 +584,28 @@ public class WorkspaceRuntimes {
return ImmutableSet.copyOf(statuses.asMap().keySet());
}
+ /**
+ * Gets the workspaces identifiers owned by given user. If an identifier is present in set then
+ * that workspace wasn't stopped at the moment of method execution.
+ *
+ * @param owner
+ * @return workspaces identifiers for those workspaces that are active(not stopped), or an empty
+ * set if there is no a single active workspace
+ * @throws ServerException
+ * @throws InfrastructureException
+ */
+ public Set getActive(String owner) throws ServerException, InfrastructureException {
+ Set activeForOwner = new HashSet<>();
+ Set active = getActive();
+ for (String workspaceId : active) {
+ InternalRuntime> internalRuntime = getInternalRuntime(workspaceId);
+ if (owner.equals(internalRuntime.getOwner())) {
+ activeForOwner.add(workspaceId);
+ }
+ }
+ return ImmutableSet.copyOf(activeForOwner);
+ }
+
/**
* Returns true if there is at least one workspace active(it's status is different from {@link
* WorkspaceStatus#STOPPED}), otherwise returns false.
@@ -606,6 +629,23 @@ public class WorkspaceRuntimes {
.collect(toSet());
}
+ /**
+ * Gets the list of workspace id's which are currently starting or stopping on given node and
+ * owned by given user id. (it's status is {@link WorkspaceStatus#STARTING} or {@link
+ * WorkspaceStatus#STOPPING})
+ */
+ public Set getInProgress(String owner) throws ServerException, InfrastructureException {
+ Set inProgressForOwner = new HashSet<>();
+ Set inProgress = getInProgress();
+ for (String workspaceId : inProgress) {
+ InternalRuntime> internalRuntime = getInternalRuntime(workspaceId);
+ if (owner.equals(internalRuntime.getOwner())) {
+ inProgressForOwner.add(workspaceId);
+ }
+ }
+ return ImmutableSet.copyOf(inProgressForOwner);
+ }
+
/**
* Returns true if there is at least one local workspace starting or stopping (it's status is
* {@link WorkspaceStatus#STARTING} or {@link WorkspaceStatus#STOPPING}), otherwise returns false.
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 0cb5a16529..508ad2d7f2 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
@@ -23,6 +23,8 @@ import static org.eclipse.che.api.core.model.workspace.config.MachineConfig.MEMO
import static org.eclipse.che.api.workspace.server.devfile.Constants.CURRENT_API_VERSION;
import static org.eclipse.che.api.workspace.shared.Constants.CREATED_ATTRIBUTE_NAME;
import static org.eclipse.che.api.workspace.shared.Constants.ERROR_MESSAGE_ATTRIBUTE_NAME;
+import static org.eclipse.che.api.workspace.shared.Constants.LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE;
+import static org.eclipse.che.api.workspace.shared.Constants.LAST_ACTIVITY_TIME;
import static org.eclipse.che.api.workspace.shared.Constants.STOPPED_ABNORMALLY_ATTRIBUTE_NAME;
import static org.eclipse.che.api.workspace.shared.Constants.STOPPED_ATTRIBUTE_NAME;
import static org.eclipse.che.api.workspace.shared.Constants.UPDATED_ATTRIBUTE_NAME;
@@ -53,7 +55,10 @@ import static org.testng.Assert.assertTrue;
import static org.testng.util.Strings.isNullOrEmpty;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.time.Clock;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -76,6 +81,7 @@ import org.eclipse.che.api.core.model.workspace.devfile.Devfile;
import org.eclipse.che.api.core.model.workspace.runtime.Machine;
import org.eclipse.che.api.core.model.workspace.runtime.MachineStatus;
import org.eclipse.che.api.core.notification.EventService;
+import org.eclipse.che.api.user.server.PreferenceManager;
import org.eclipse.che.api.workspace.server.devfile.convert.DevfileConverter;
import org.eclipse.che.api.workspace.server.devfile.exception.DevfileFormatException;
import org.eclipse.che.api.workspace.server.devfile.validator.DevfileIntegrityValidator;
@@ -130,6 +136,7 @@ public class WorkspaceManagerTest {
@Mock private WorkspaceValidator validator;
@Mock private DevfileConverter devfileConverter;
@Mock private DevfileIntegrityValidator devfileIntegrityValidator;
+ @Mock private PreferenceManager preferenceManager;
@Captor private ArgumentCaptor workspaceCaptor;
@@ -143,6 +150,7 @@ public class WorkspaceManagerTest {
runtimes,
eventService,
accountManager,
+ preferenceManager,
validator,
devfileIntegrityValidator);
lenient()
@@ -829,6 +837,62 @@ public class WorkspaceManagerTest {
// then exception is thrown
}
+ @Test
+ public void startsWorkspaceShouldCleanPreferences() throws Exception {
+ final WorkspaceImpl workspace = createAndMockWorkspace();
+ Runtime runtime = mockRuntime(workspace, WorkspaceStatus.RUNNING);
+ workspace.setRuntime(runtime);
+ Map pref = new HashMap<>(2);
+ pref.put(LAST_ACTIVITY_TIME, "now");
+ pref.put(LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE, "my_namespace");
+ when(preferenceManager.find("owner")).thenReturn(pref);
+
+ mockAnyWorkspaceStart();
+ workspaceManager.startWorkspace(
+ workspace.getId(), workspace.getConfig().getDefaultEnv(), emptyMap());
+
+ verify(runtimes).startAsync(workspace, workspace.getConfig().getDefaultEnv(), emptyMap());
+ assertNotNull(workspace.getAttributes().get(UPDATED_ATTRIBUTE_NAME));
+ verify(preferenceManager)
+ .remove("owner", Arrays.asList(LAST_ACTIVITY_TIME, LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE));
+ }
+
+ @Test
+ public void stopsLastWorkspaceShouldUpdatePreferences() throws Exception {
+ final WorkspaceImpl workspace = createAndMockWorkspace(createConfig(), NAMESPACE_1);
+ mockRuntime(workspace, RUNNING);
+ mockAnyWorkspaceStop();
+ when(runtimes.isAnyActive()).thenReturn(false);
+ long epochSecond = Clock.systemDefaultZone().instant().getEpochSecond();
+ workspaceManager.stopWorkspace(workspace.getId(), emptyMap());
+ verify(runtimes).stopAsync(workspace, emptyMap());
+ verify(workspaceDao).update(workspaceCaptor.capture());
+ verify(preferenceManager).find("owner");
+ Map pref = new HashMap<>(2);
+ pref.put(LAST_ACTIVITY_TIME, Long.toString(epochSecond));
+ pref.put(LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE, INFRA_NAMESPACE);
+ verify(preferenceManager).update("owner", pref);
+ }
+
+ @Test
+ public void stopsWorkspaceShouldNotUpdatePreferencesIfOtherWorkspaceRunning() throws Exception {
+ final WorkspaceImpl workspace = createAndMockWorkspace(createConfig(), NAMESPACE_1);
+ mockRuntime(workspace, RUNNING);
+ mockAnyWorkspaceStop();
+ when(runtimes.getActive(anyString()))
+ .thenReturn(ImmutableSet.of(NameGenerator.generate("ws", 5)));
+ long millis = System.currentTimeMillis();
+ workspaceManager.stopWorkspace(workspace.getId(), emptyMap());
+ verify(runtimes).stopAsync(workspace, emptyMap());
+ verify(workspaceDao).update(workspaceCaptor.capture());
+ Map pref = new HashMap<>(2);
+ pref.put(LAST_ACTIVITY_TIME, Long.toString(millis));
+ pref.put(LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE, INFRA_NAMESPACE);
+ verify(preferenceManager, never()).find("owner");
+ verify(preferenceManager, never())
+ .remove("owner", Arrays.asList(LAST_ACTIVITY_TIME, LAST_ACTIVE_INFRASTRUCTURE_NAMESPACE));
+ }
+
private void mockRuntimeStatus(WorkspaceImpl workspace, WorkspaceStatus status) {
lenient().when(runtimes.getStatus(workspace.getId())).thenReturn(status);
}
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 4f22c9c363..f14d7a9b83 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
@@ -827,6 +827,67 @@ public class WorkspaceRuntimesTest {
assertTrue(active.containsAll(asList("ws1", "ws2", "ws3")));
}
+ @Test
+ public void shouldReturnRuntimesIdsOfActiveWorkspacesForGivenOwner() throws Exception {
+ // given
+ String ws1 = generate("workspace", 6);
+ String ws2 = generate("workspace", 6);
+ String ws3 = generate("workspace", 6);
+ String owner = generate("user", 6);
+
+ when(statuses.asMap())
+ .thenReturn(
+ ImmutableMap.of(
+ ws1, WorkspaceStatus.STARTING,
+ ws2, WorkspaceStatus.RUNNING,
+ ws3, WorkspaceStatus.STOPPING));
+
+ RuntimeIdentityImpl runtimeIdentity1 =
+ new RuntimeIdentityImpl(ws1, generate("env", 6), owner, generate("infraNamespace", 6));
+
+ RuntimeIdentityImpl runtimeIdentity2 =
+ new RuntimeIdentityImpl(
+ ws2, generate("env", 6), generate("user", 6), generate("infraNamespace", 6));
+
+ RuntimeIdentityImpl runtimeIdentity3 =
+ new RuntimeIdentityImpl(
+ ws3, generate("env", 6), generate("user", 6), generate("infraNamespace", 6));
+
+ mockWorkspaceWithConfig(runtimeIdentity1);
+ mockWorkspaceWithConfig(runtimeIdentity2);
+ mockWorkspaceWithConfig(runtimeIdentity3);
+
+ RuntimeContext context1 = mockContext(runtimeIdentity1);
+ RuntimeContext context2 = mockContext(runtimeIdentity2);
+ RuntimeContext context3 = mockContext(runtimeIdentity3);
+
+ when(context1.getRuntime())
+ .thenReturn(new TestInternalRuntime(context1, emptyMap(), WorkspaceStatus.STARTING));
+ when(context2.getRuntime())
+ .thenReturn(new TestInternalRuntime(context2, emptyMap(), WorkspaceStatus.RUNNING));
+ when(context3.getRuntime())
+ .thenReturn(new TestInternalRuntime(context3, emptyMap(), WorkspaceStatus.STOPPING));
+
+ doReturn(context1).when(infrastructure).prepare(eq(runtimeIdentity1), any());
+ doReturn(context2).when(infrastructure).prepare(eq(runtimeIdentity2), any());
+ doReturn(context3).when(infrastructure).prepare(eq(runtimeIdentity3), any());
+
+ Set identities =
+ ImmutableSet.of(runtimeIdentity1, runtimeIdentity2, runtimeIdentity3);
+
+ doReturn(identities).when(infrastructure).getIdentities();
+
+ InternalEnvironment internalEnvironment = mock(InternalEnvironment.class);
+ doReturn(internalEnvironment).when(testEnvFactory).create(any(Environment.class));
+
+ // when
+ Set active = runtimes.getActive(owner);
+
+ // then
+ assertEquals(active.size(), 1);
+ assertTrue(active.containsAll(asList(ws1)));
+ }
+
@Test
public void shouldReturnWorkspaceIdsOfRunningRuntimes() {
// given