Shutdown Asynchronous Pod after some idle time (#17615)

* Shutdown Asynchronous Pod after some idle time

Signed-off-by: Vitalii Parfonov <vparfono@redhat.com>

Co-authored-by: Ilya Buziuk <ibuziuk@redhat.com>
7.20.x
Vitalii Parfonov 2020-08-13 17:17:06 +03:00 committed by GitHub
parent 137661257e
commit 3d27158968
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 694 additions and 0 deletions

View File

@ -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

View File

@ -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:
*
* <ul>
* <li>che.infra.kubernetes.namespace.default=<username>-che
* <li>che.infra.kubernetes.namespace.allow_user_defined=false
* <li>che.infra.kubernetes.pvc.strategy=common
* <li>che.limits.user.workspaces.run.count=1
* </ul>
*/
private boolean isAsyncStoragePodCanBeRun(
String pvcStrategy,
boolean allowUserDefinedNamespaces,
String defaultNamespaceName,
int runtimesPerUser) {
return !allowUserDefinedNamespaces
&& COMMON_STRATEGY.equals(pvcStrategy)
&& runtimesPerUser == 1
&& !isNullOrEmpty(defaultNamespaceName)
&& defaultNamespaceName.contains("<username>");
}
@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<String, String> 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<Pod, DoneablePod> doneablePodResource =
kubernetesClientFactory
.create()
.pods()
.inNamespace(namespace)
.withName(ASYNC_STORAGE);
if (doneablePodResource.get() != null) {
doneablePodResource.delete();
}
}
} catch (InfrastructureException | ServerException e) {
LOG.error(e.getMessage(), e);
}
}
}
}
}

View File

@ -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<String, String> userPref;
@Mock private KubernetesClientFactory kubernetesClientFactory;
@Mock private UserManager userManager;
@Mock private PreferenceManager preferenceManager;
@Mock private WorkspaceRuntimes runtimes;
@Mock private KubernetesClient kubernetesClient;
@Mock private PodResource<Pod, DoneablePod> 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<UserImpl> 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,
"<username>",
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,
"<username>",
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,
"<username>",
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<UserImpl> 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,
"<username>",
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,
"<username>",
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,
"<username>",
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,
"<foo-bar>",
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,
"<foo-bar>",
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,
"<username>",
1);
when(preferenceManager.find(USER_ID)).thenReturn(emptyMap()); // no records in user preferences
watcher.check();
verifyNoMoreInteractions(preferenceManager);
verifyNoMoreInteractions(kubernetesClientFactory);
verifyNoMoreInteractions(podResource);
}
}

View File

@ -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);
}
}

View File

@ -74,6 +74,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-model</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-user</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-workspace</artifactId>

View File

@ -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;

View File

@ -266,6 +266,7 @@ public class LimitsCheckingWorkspaceManagerTest {
null,
null,
null,
null,
maxRamPerEnv,
environmentRamCalculator,
resourceManager,

View File

@ -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() {}
}

View File

@ -102,6 +102,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-system-shared</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-user</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-workspace-shared</artifactId>

View File

@ -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<String, String> 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<String> 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(

View File

@ -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<String> getActive(String owner) throws ServerException, InfrastructureException {
Set<String> activeForOwner = new HashSet<>();
Set<String> 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<String> getInProgress(String owner) throws ServerException, InfrastructureException {
Set<String> inProgressForOwner = new HashSet<>();
Set<String> 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.

View File

@ -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<WorkspaceImpl> 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<String, String> 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<String, String> 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<String, String> 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);
}

View File

@ -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<RuntimeIdentity> 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<String> active = runtimes.getActive(owner);
// then
assertEquals(active.size(), 1);
assertTrue(active.containsAll(asList(ws1)));
}
@Test
public void shouldReturnWorkspaceIdsOfRunningRuntimes() {
// given