From 3d271589685eecba3ce0bb7d3f2393e5fd650063 Mon Sep 17 00:00:00 2001 From: Vitalii Parfonov Date: Thu, 13 Aug 2020 17:17:06 +0300 Subject: [PATCH] Shutdown Asynchronous Pod after some idle time (#17615) * Shutdown Asynchronous Pod after some idle time Signed-off-by: Vitalii Parfonov Co-authored-by: Ilya Buziuk --- .../webapp/WEB-INF/classes/che/che.properties | 7 + .../provision/AsyncStoragePodWatcher.java | 156 +++++++++ .../provision/AsyncStoragePodWatcherTest.java | 302 ++++++++++++++++++ .../openshift/OpenShiftInfraModule.java | 2 + .../api/che-multiuser-api-resource/pom.xml | 4 + .../LimitsCheckingWorkspaceManager.java | 3 + .../LimitsCheckingWorkspaceManagerTest.java | 1 + .../che/api/workspace/shared/Constants.java | 10 + wsmaster/che-core-api-workspace/pom.xml | 4 + .../workspace/server/WorkspaceManager.java | 40 +++ .../workspace/server/WorkspaceRuntimes.java | 40 +++ .../server/WorkspaceManagerTest.java | 64 ++++ .../server/WorkspaceRuntimesTest.java | 61 ++++ 13 files changed, 694 insertions(+) create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/AsyncStoragePodWatcher.java create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/AsyncStoragePodWatcherTest.java 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