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
parent
137661257e
commit
3d27158968
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ public class LimitsCheckingWorkspaceManagerTest {
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
maxRamPerEnv,
|
||||
environmentRamCalculator,
|
||||
resourceManager,
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue