diff --git a/assembly/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml b/assembly/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml
index 3afe32e5e8..bc32faf5b7 100644
--- a/assembly/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml
+++ b/assembly/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml
@@ -66,6 +66,7 @@
org.eclipse.che.multiuser.organization.spi.impl.OrganizationDistributedResourcesImpl
org.eclipse.che.api.workspace.activity.WorkspaceExpiration
+ org.eclipse.che.api.workspace.activity.WorkspaceActivity
org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyImpl
org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/SystemDomain.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/SystemDomain.java
index 80e6e1931c..81040154e8 100644
--- a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/SystemDomain.java
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/SystemDomain.java
@@ -11,9 +11,10 @@
*/
package org.eclipse.che.multiuser.api.permission.server;
+import static java.util.stream.Collectors.toList;
+
import java.util.List;
import java.util.Set;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
@@ -34,13 +35,15 @@ public class SystemDomain extends AbstractPermissionsDomain allowedActions) {
super(
DOMAIN_ID,
- Stream.concat(allowedActions.stream(), Stream.of(MANAGE_SYSTEM_ACTION))
- .collect(Collectors.toList()),
+ Stream.concat(
+ allowedActions.stream(), Stream.of(MANAGE_SYSTEM_ACTION, MONITOR_SYSTEM_ACTION))
+ .collect(toList()),
false);
}
diff --git a/multiuser/api/che-multiuser-api-workspace-activity/src/test/java/org/eclipse/che/multiuser/api/workspace/activity/MultiUserWorkspaceActivityManagerTest.java b/multiuser/api/che-multiuser-api-workspace-activity/src/test/java/org/eclipse/che/multiuser/api/workspace/activity/MultiUserWorkspaceActivityManagerTest.java
index e82d30dd7a..d2b08d61b0 100644
--- a/multiuser/api/che-multiuser-api-workspace-activity/src/test/java/org/eclipse/che/multiuser/api/workspace/activity/MultiUserWorkspaceActivityManagerTest.java
+++ b/multiuser/api/che-multiuser-api-workspace-activity/src/test/java/org/eclipse/che/multiuser/api/workspace/activity/MultiUserWorkspaceActivityManagerTest.java
@@ -21,7 +21,6 @@ import org.eclipse.che.account.shared.model.Account;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.notification.EventSubscriber;
import org.eclipse.che.api.workspace.activity.WorkspaceActivityDao;
-import org.eclipse.che.api.workspace.activity.WorkspaceExpiration;
import org.eclipse.che.api.workspace.server.WorkspaceManager;
import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent;
@@ -89,8 +88,8 @@ public class MultiUserWorkspaceActivityManagerTest {
activityManager.update(wsId, activityTime);
- WorkspaceExpiration expected = new WorkspaceExpiration(wsId, activityTime + USER_LIMIT_TIMEOUT);
- verify(workspaceActivityDao, times(1)).setExpiration(eq(expected));
+ verify(workspaceActivityDao, times(1))
+ .setExpirationTime(eq(wsId), eq(activityTime + USER_LIMIT_TIMEOUT));
verify(resourceManager).getAvailableResources(eq("account123"));
}
@@ -102,8 +101,8 @@ public class MultiUserWorkspaceActivityManagerTest {
activityManager.update(wsId, activityTime);
- WorkspaceExpiration expected = new WorkspaceExpiration(wsId, activityTime + DEFAULT_TIMEOUT);
- verify(workspaceActivityDao, times(1)).setExpiration(eq(expected));
+ verify(workspaceActivityDao, times(1))
+ .setExpirationTime(eq(wsId), eq(activityTime + DEFAULT_TIMEOUT));
verify(resourceManager).getAvailableResources(eq("account123"));
}
}
diff --git a/multiuser/integration-tests/che-multiuser-mysql-tck/pom.xml b/multiuser/integration-tests/che-multiuser-mysql-tck/pom.xml
index 7e0d24f42f..09c5a8ffb4 100644
--- a/multiuser/integration-tests/che-multiuser-mysql-tck/pom.xml
+++ b/multiuser/integration-tests/che-multiuser-mysql-tck/pom.xml
@@ -252,7 +252,11 @@
jdbc.port:3306
- ready for connections
+
+
+ 3306
+
+
diff --git a/multiuser/permission/che-multiuser-permission-workspace-activity/pom.xml b/multiuser/permission/che-multiuser-permission-workspace-activity/pom.xml
index fe57451a7d..65af6e31a0 100644
--- a/multiuser/permission/che-multiuser-permission-workspace-activity/pom.xml
+++ b/multiuser/permission/che-multiuser-permission-workspace-activity/pom.xml
@@ -30,10 +30,18 @@
org.eclipse.che.core
che-core-api-core
+
+ org.eclipse.che.core
+ che-core-api-model
+
org.eclipse.che.core
che-core-commons-test
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission
+
org.eclipse.che.multiuser
che-multiuser-permission-workspace
diff --git a/multiuser/permission/che-multiuser-permission-workspace-activity/src/main/java/org/eclipse/che/multiuser/permission/workspace/activity/ActivityPermissionsFilter.java b/multiuser/permission/che-multiuser-permission-workspace-activity/src/main/java/org/eclipse/che/multiuser/permission/workspace/activity/ActivityPermissionsFilter.java
index 751fc7539c..e7be60d2e5 100644
--- a/multiuser/permission/che-multiuser-permission-workspace-activity/src/main/java/org/eclipse/che/multiuser/permission/workspace/activity/ActivityPermissionsFilter.java
+++ b/multiuser/permission/che-multiuser-permission-workspace-activity/src/main/java/org/eclipse/che/multiuser/permission/workspace/activity/ActivityPermissionsFilter.java
@@ -17,6 +17,7 @@ import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.everrest.CheMethodInvokerFilter;
+import org.eclipse.che.multiuser.api.permission.server.SystemDomain;
import org.eclipse.che.multiuser.permission.workspace.server.WorkspaceDomain;
import org.everrest.core.Filter;
import org.everrest.core.resource.GenericResourceMethod;
@@ -36,19 +37,24 @@ public class ActivityPermissionsFilter extends CheMethodInvokerFilter {
final String methodName = genericResourceMethod.getMethod().getName();
final Subject currentSubject = EnvironmentContext.getCurrent().getSubject();
+ String domain;
String action;
- String workspaceId;
+ String instance;
switch (methodName) {
case "active":
- {
- workspaceId = ((String) arguments[0]);
- action = WorkspaceDomain.USE;
- break;
- }
+ domain = WorkspaceDomain.DOMAIN_ID;
+ instance = (String) arguments[0];
+ action = WorkspaceDomain.USE;
+ break;
+ case "getWorkspacesByActivity":
+ domain = SystemDomain.DOMAIN_ID;
+ instance = null;
+ action = SystemDomain.MONITOR_SYSTEM_ACTION;
+ break;
default:
throw new ForbiddenException("The user does not have permission to perform this operation");
}
- currentSubject.checkPermission(WorkspaceDomain.DOMAIN_ID, workspaceId, action);
+ currentSubject.checkPermission(domain, instance, action);
}
}
diff --git a/multiuser/permission/che-multiuser-permission-workspace-activity/src/test/java/org/eclipse/che/multiuser/permission/workspace/activity/ActivityPermissionsFilterTest.java b/multiuser/permission/che-multiuser-permission-workspace-activity/src/test/java/org/eclipse/che/multiuser/permission/workspace/activity/ActivityPermissionsFilterTest.java
index c264820ec8..99a7dc7bad 100644
--- a/multiuser/permission/che-multiuser-permission-workspace-activity/src/test/java/org/eclipse/che/multiuser/permission/workspace/activity/ActivityPermissionsFilterTest.java
+++ b/multiuser/permission/che-multiuser-permission-workspace-activity/src/test/java/org/eclipse/che/multiuser/permission/workspace/activity/ActivityPermissionsFilterTest.java
@@ -23,10 +23,15 @@ import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import com.jayway.restassured.response.Response;
+import java.util.Collections;
import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.Pages;
+import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
import org.eclipse.che.api.workspace.activity.WorkspaceActivityService;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.multiuser.api.permission.server.SystemDomain;
import org.eclipse.che.multiuser.permission.workspace.server.WorkspaceDomain;
import org.everrest.assured.EverrestJetty;
import org.everrest.core.Filter;
@@ -93,6 +98,52 @@ public class ActivityPermissionsFilterTest {
assertEquals(response.getStatusCode(), 403);
}
+ @Test
+ public void shouldCheckPermissionsOnGettingActivity() throws Exception {
+ // simulate output to not get a 204, which should never happen in reality
+ when(service.getWorkspacesByActivity(
+ eq(WorkspaceStatus.RUNNING), eq(-1L), eq(-1L), eq(Pages.DEFAULT_PAGE_SIZE), eq(0L)))
+ .thenReturn(
+ javax.ws.rs.core.Response.ok(new Page(Collections.emptyList(), 0, 1, 0))
+ .build());
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .when()
+ .get(SECURE_PATH + "/activity?status=RUNNING");
+
+ assertEquals(response.getStatusCode(), 200);
+ verify(service)
+ .getWorkspacesByActivity(
+ eq(WorkspaceStatus.RUNNING), eq(-1L), eq(-1L), eq(Pages.DEFAULT_PAGE_SIZE), eq(0L));
+ verify(subject)
+ .checkPermission(
+ eq(SystemDomain.DOMAIN_ID), eq(null), eq(SystemDomain.MONITOR_SYSTEM_ACTION));
+ }
+
+ @Test
+ public void shouldThrowExceptionWhenNotAuthzdToGetActivity() throws Exception {
+ doThrow(
+ new ForbiddenException(
+ "The user does not have permission to "
+ + SystemDomain.MONITOR_SYSTEM_ACTION
+ + " workspace with id 'workspace123'"))
+ .when(subject)
+ .checkPermission(
+ eq(SystemDomain.DOMAIN_ID), eq(null), eq(SystemDomain.MONITOR_SYSTEM_ACTION));
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .when()
+ .get(SECURE_PATH + "/activity?status=STARTING");
+
+ assertEquals(response.getStatusCode(), 403);
+ }
+
@Test(expectedExceptions = ForbiddenException.class)
public void shouldThrowExceptionWhenCallingUnlistedMethod() throws Exception {
diff --git a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/InmemoryWorkspaceActivityDao.java b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/InmemoryWorkspaceActivityDao.java
index 23774b1088..baf0b324bb 100644
--- a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/InmemoryWorkspaceActivityDao.java
+++ b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/InmemoryWorkspaceActivityDao.java
@@ -13,38 +13,128 @@ package org.eclipse.che.api.workspace.activity;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import javax.inject.Singleton;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
/**
- * Inmemory workspaces expiration times storage.
+ * In-memory workspaces expiration times storage.
*
* @author Max Shaposhnik (mshaposh@redhat.com)
*/
@Singleton
public class InmemoryWorkspaceActivityDao implements WorkspaceActivityDao {
- private final Map activeWorkspaces = new ConcurrentHashMap<>();
+ private final Map workspaceActivities = new ConcurrentHashMap<>();
@Override
public void setExpiration(WorkspaceExpiration expiration) {
- activeWorkspaces.put(expiration.getWorkspaceId(), expiration.getExpiration());
+ setExpirationTime(expiration.getWorkspaceId(), expiration.getExpiration());
+ }
+
+ @Override
+ public void setExpirationTime(String workspaceId, long expirationTime) {
+ findActivity(workspaceId).setExpiration(expirationTime);
}
@Override
public void removeExpiration(String workspaceId) {
- activeWorkspaces.remove(workspaceId);
+ findActivity(workspaceId).setExpiration(null);
}
@Override
public List findExpired(long timestamp) {
- return activeWorkspaces
- .entrySet()
+ return workspaceActivities
+ .values()
.stream()
- .filter(e -> e.getValue() < timestamp)
- .map(Entry::getKey)
+ .filter(a -> a.getExpiration() != null && a.getExpiration() < timestamp)
+ .map(WorkspaceActivity::getWorkspaceId)
.collect(Collectors.toList());
}
+
+ @Override
+ public void setCreatedTime(String workspaceId, long createdTimestamp) {
+ WorkspaceActivity activity = findActivity(workspaceId);
+ activity.setCreated(createdTimestamp);
+ if (activity.getStatus() == null) {
+ activity.setStatus(WorkspaceStatus.STOPPED);
+ }
+ }
+
+ @Override
+ public void setStatusChangeTime(String workspaceId, WorkspaceStatus status, long timestamp)
+ throws ServerException {
+ WorkspaceActivity a = findActivity(workspaceId);
+ switch (status) {
+ case STARTING:
+ a.setLastStarting(timestamp);
+ break;
+ case RUNNING:
+ a.setLastRunning(timestamp);
+ break;
+ case STOPPING:
+ a.setLastStopping(timestamp);
+ break;
+ case STOPPED:
+ a.setLastStopped(timestamp);
+ break;
+ default:
+ throw new ServerException("Unhandled workspace status: " + status);
+ }
+ }
+
+ @Override
+ public Page findInStatusSince(
+ long timestamp, WorkspaceStatus status, int maxItems, long skipCount) {
+ List all =
+ workspaceActivities
+ .values()
+ .stream()
+ .filter(a -> a.getStatus() == status)
+ .filter(
+ a -> {
+ switch (status) {
+ case STOPPED:
+ return isGreater(a.getLastStopped(), timestamp);
+ case STOPPING:
+ return isGreater(a.getLastStopped(), timestamp);
+ case RUNNING:
+ return isGreater(a.getLastStopped(), timestamp);
+ case STARTING:
+ return isGreater(a.getLastStopped(), timestamp);
+ default:
+ return false;
+ }
+ })
+ .map(WorkspaceActivity::getWorkspaceId)
+ .collect(Collectors.toList());
+
+ int total = all.size();
+ int from = skipCount > total ? total : (int) skipCount;
+ int to = from + maxItems;
+ if (to > total) {
+ to = total;
+ }
+
+ List page = all.subList(from, to);
+
+ return new Page<>(page, skipCount, maxItems, all.size());
+ }
+
+ @Override
+ public WorkspaceActivity findActivity(String workspaceId) {
+ return workspaceActivities.computeIfAbsent(workspaceId, __ -> new WorkspaceActivity());
+ }
+
+ @Override
+ public void removeActivity(String workspaceId) throws ServerException {
+ workspaceActivities.remove(workspaceId);
+ }
+
+ private boolean isGreater(Long value, long threshold) {
+ return value != null && value > threshold;
+ }
}
diff --git a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/JpaWorkspaceActivityDao.java b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/JpaWorkspaceActivityDao.java
index 30119eb7f3..f4d2579076 100644
--- a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/JpaWorkspaceActivityDao.java
+++ b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/JpaWorkspaceActivityDao.java
@@ -15,16 +15,15 @@ import static java.util.Objects.requireNonNull;
import com.google.inject.persist.Transactional;
import java.util.List;
+import java.util.function.Consumer;
import java.util.stream.Collectors;
-import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.persistence.EntityManager;
+import org.eclipse.che.api.core.Page;
import org.eclipse.che.api.core.ServerException;
-import org.eclipse.che.api.core.notification.EventService;
-import org.eclipse.che.api.workspace.server.event.BeforeWorkspaceRemovedEvent;
-import org.eclipse.che.core.db.cascade.CascadeEventSubscriber;
+import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
/**
* JPA workspaces expiration times storage.
@@ -38,81 +37,195 @@ public class JpaWorkspaceActivityDao implements WorkspaceActivityDao {
@Override
public void setExpiration(WorkspaceExpiration expiration) throws ServerException {
- requireNonNull(expiration, "Required non-null expiration object");
- try {
- doCreateOrUpdate(expiration);
- } catch (RuntimeException x) {
- throw new ServerException(x.getLocalizedMessage(), x);
- }
+ setExpirationTime(expiration.getWorkspaceId(), expiration.getExpiration());
+ }
+
+ @Override
+ public void setExpirationTime(String workspaceId, long expirationTime) throws ServerException {
+ requireNonNull(workspaceId, "Required non-null workspace id");
+ doUpdate(workspaceId, a -> a.setExpiration(expirationTime));
}
@Override
public void removeExpiration(String workspaceId) throws ServerException {
- requireNonNull(workspaceId, "Required non-null id");
+ requireNonNull(workspaceId, "Required non-null workspace id");
+ doUpdateOptionally(workspaceId, a -> a.setExpiration(null));
+ }
+
+ @Override
+ @Transactional(rollbackOn = ServerException.class)
+ public List findExpired(long timestamp) throws ServerException {
try {
- doRemove(workspaceId);
+ return managerProvider
+ .get()
+ .createNamedQuery("WorkspaceActivity.getExpired", WorkspaceActivity.class)
+ .setParameter("expiration", timestamp)
+ .getResultList()
+ .stream()
+ .map(WorkspaceActivity::getWorkspaceId)
+ .collect(Collectors.toList());
} catch (RuntimeException x) {
throw new ServerException(x.getLocalizedMessage(), x);
}
}
@Override
- public List findExpired(long timestamp) throws ServerException {
+ @Transactional(rollbackOn = ServerException.class)
+ public void removeActivity(String workspaceId) throws ServerException {
try {
- return doFindExpired(timestamp);
+ EntityManager em = managerProvider.get();
+ WorkspaceActivity activity = em.find(WorkspaceActivity.class, workspaceId);
+ if (activity != null) {
+ em.remove(activity);
+ em.flush();
+ }
} catch (RuntimeException x) {
throw new ServerException(x.getLocalizedMessage(), x);
}
}
- @Transactional
- protected List doFindExpired(long timestamp) {
- return managerProvider
- .get()
- .createNamedQuery("WorkspaceExpiration.getExpired", WorkspaceExpiration.class)
- .setParameter("expiration", timestamp)
- .getResultList()
- .stream()
- .map(WorkspaceExpiration::getWorkspaceId)
- .collect(Collectors.toList());
+ @Override
+ public void setCreatedTime(String workspaceId, long createdTimestamp) throws ServerException {
+ requireNonNull(workspaceId, "Required non-null workspace id");
+ doUpdate(
+ workspaceId,
+ a -> {
+ a.setCreated(createdTimestamp);
+
+ // We might just have created the activity record and we need to initialize the status
+ // to something. Since a created workspace is implicitly stopped, let's record it like
+ // that.
+ // If any status change event was already captured, the status would have been set
+ // accordingly already.
+ if (a.getStatus() == null) {
+ a.setStatus(WorkspaceStatus.STOPPED);
+ }
+ });
}
- @Transactional
- protected void doCreateOrUpdate(WorkspaceExpiration expiration) {
- final EntityManager manager = managerProvider.get();
- if (manager.find(WorkspaceExpiration.class, expiration.getWorkspaceId()) == null) {
- manager.persist(expiration);
- } else {
- manager.merge(expiration);
+ @Override
+ public void setStatusChangeTime(String workspaceId, WorkspaceStatus status, long timestamp)
+ throws ServerException {
+ requireNonNull(workspaceId, "Required non-null workspace id");
+
+ Consumer update;
+ switch (status) {
+ case RUNNING:
+ update =
+ a -> {
+ a.setStatus(status);
+ a.setLastRunning(timestamp);
+ };
+ break;
+ case STARTING:
+ update =
+ a -> {
+ a.setStatus(status);
+ a.setLastStarting(timestamp);
+ };
+ break;
+ case STOPPED:
+ update =
+ a -> {
+ a.setStatus(status);
+ a.setLastStopped(timestamp);
+ };
+ break;
+ case STOPPING:
+ update =
+ a -> {
+ a.setStatus(status);
+ a.setLastStopping(timestamp);
+ };
+ break;
+ default:
+ throw new ServerException("Unhandled workspace status: " + status);
}
- manager.flush();
+
+ doUpdate(workspaceId, update);
}
- @Transactional
- protected void doRemove(String workspaceId) {
- final EntityManager manager = managerProvider.get();
- final WorkspaceExpiration expiration = manager.find(WorkspaceExpiration.class, workspaceId);
- if (expiration != null) {
- manager.remove(expiration);
- manager.flush();
+ @Override
+ @Transactional(rollbackOn = ServerException.class)
+ public Page findInStatusSince(
+ long timestamp, WorkspaceStatus status, int maxItems, long skipCount) throws ServerException {
+ try {
+ String queryName = "WorkspaceActivity.get" + firstUpperCase(status.name()) + "Since";
+ String countQueryName = queryName + "Count";
+
+ long count =
+ managerProvider
+ .get()
+ .createNamedQuery(countQueryName, Long.class)
+ .setParameter("time", timestamp)
+ .getSingleResult();
+
+ List data =
+ managerProvider
+ .get()
+ .createNamedQuery(queryName, String.class)
+ .setParameter("time", timestamp)
+ .setFirstResult((int) skipCount)
+ .setMaxResults(maxItems)
+ .getResultList();
+
+ return new Page<>(data, skipCount, maxItems, count);
+ } catch (RuntimeException e) {
+ throw new ServerException(e.getLocalizedMessage(), e);
}
}
- @Singleton
- public static class RemoveExpirationBeforeWorkspaceRemovedEventSubscriber
- extends CascadeEventSubscriber {
-
- @Inject private EventService eventService;
- @Inject private WorkspaceActivityDao workspaceActivityDao;
-
- @PostConstruct
- public void subscribe() {
- eventService.subscribe(this, BeforeWorkspaceRemovedEvent.class);
+ @Override
+ @Transactional(rollbackOn = ServerException.class)
+ public WorkspaceActivity findActivity(String workspaceId) throws ServerException {
+ try {
+ EntityManager em = managerProvider.get();
+ return em.find(WorkspaceActivity.class, workspaceId);
+ } catch (RuntimeException x) {
+ throw new ServerException(x.getLocalizedMessage(), x);
}
+ }
- @Override
- public void onCascadeEvent(BeforeWorkspaceRemovedEvent event) throws Exception {
- workspaceActivityDao.removeExpiration(event.getWorkspace().getId());
+ @Transactional(rollbackOn = ServerException.class)
+ protected void doUpdate(String workspaceId, Consumer updater)
+ throws ServerException {
+ doUpdate(false, workspaceId, updater);
+ }
+
+ @Transactional(rollbackOn = ServerException.class)
+ protected void doUpdateOptionally(String workspaceId, Consumer updater)
+ throws ServerException {
+ doUpdate(true, workspaceId, updater);
+ }
+
+ private void doUpdate(boolean optional, String workspaceId, Consumer updater)
+ throws ServerException {
+ try {
+ EntityManager em = managerProvider.get();
+ WorkspaceActivity activity = em.find(WorkspaceActivity.class, workspaceId);
+ if (activity == null) {
+ if (optional) {
+ return;
+ }
+ activity = new WorkspaceActivity();
+ activity.setWorkspaceId(workspaceId);
+
+ updater.accept(activity);
+
+ em.persist(activity);
+ } else {
+ updater.accept(activity);
+
+ em.merge(activity);
+ }
+
+ em.flush();
+ } catch (RuntimeException x) {
+ throw new ServerException(x.getLocalizedMessage(), x);
}
}
+
+ private static String firstUpperCase(String str) {
+ return Character.toUpperCase(str.charAt(0)) + str.substring(1).toLowerCase();
+ }
}
diff --git a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivity.java b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivity.java
new file mode 100644
index 0000000000..e50cea2b9a
--- /dev/null
+++ b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivity.java
@@ -0,0 +1,226 @@
+/*
+ * 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.api.workspace.activity;
+
+import java.util.Objects;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
+
+@Entity
+@Table(name = "che_workspace_activity")
+@NamedQueries({
+ @NamedQuery(
+ name = "WorkspaceActivity.getExpired",
+ query = "SELECT a FROM WorkspaceActivity a WHERE a.expiration < :expiration"),
+ @NamedQuery(
+ name = "WorkspaceActivity.getStoppedSince",
+ query =
+ "SELECT a.workspaceId FROM WorkspaceActivity a "
+ + "WHERE a.status = org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STOPPED "
+ + "AND a.lastStopped <= :time"),
+ @NamedQuery(
+ name = "WorkspaceActivity.getStoppedSinceCount",
+ query =
+ "SELECT COUNT(a) FROM WorkspaceActivity a"
+ + " WHERE a.status = org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STOPPED"
+ + " AND a.lastStopped <= :time"),
+ @NamedQuery(
+ name = "WorkspaceActivity.getStoppingSince",
+ query =
+ "SELECT a.workspaceId FROM WorkspaceActivity a"
+ + " WHERE a.status = org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STOPPING"
+ + " AND a.lastStopping <= :time"),
+ @NamedQuery(
+ name = "WorkspaceActivity.getStoppingSinceCount",
+ query =
+ "SELECT COUNT(a) FROM WorkspaceActivity a"
+ + " WHERE a.status = org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STOPPING"
+ + " AND a.lastStopping <= :time"),
+ @NamedQuery(
+ name = "WorkspaceActivity.getRunningSince",
+ query =
+ "SELECT a.workspaceId FROM WorkspaceActivity a"
+ + " WHERE a.status = org.eclipse.che.api.core.model.workspace.WorkspaceStatus.RUNNING"
+ + " AND a.lastRunning <= :time"),
+ @NamedQuery(
+ name = "WorkspaceActivity.getRunningSinceCount",
+ query =
+ "SELECT COUNT(a) FROM WorkspaceActivity a"
+ + " WHERE a.status = org.eclipse.che.api.core.model.workspace.WorkspaceStatus.RUNNING"
+ + " AND a.lastRunning <= :time"),
+ @NamedQuery(
+ name = "WorkspaceActivity.getStartingSince",
+ query =
+ "SELECT a.workspaceId FROM WorkspaceActivity a"
+ + " WHERE a.status = org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STARTING"
+ + " AND a.lastStarting <= :time"),
+ @NamedQuery(
+ name = "WorkspaceActivity.getStartingSinceCount",
+ query =
+ "SELECT COUNT(a) FROM WorkspaceActivity a"
+ + " WHERE a.status = org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STARTING"
+ + " AND a.lastStarting <= :time"),
+})
+public class WorkspaceActivity {
+
+ @Id
+ @Column(name = "workspace_id")
+ private String workspaceId;
+
+ @Column(name = "created")
+ private Long created;
+
+ @Column(name = "last_starting")
+ private Long lastStarting;
+
+ @Column(name = "last_running")
+ private Long lastRunning;
+
+ @Column(name = "last_stopping")
+ private Long lastStopping;
+
+ @Column(name = "last_stopped")
+ private Long lastStopped;
+
+ @Column(name = "expiration")
+ private Long expiration;
+
+ @Column(name = "status")
+ @Enumerated(EnumType.STRING)
+ private WorkspaceStatus status;
+
+ public String getWorkspaceId() {
+ return workspaceId;
+ }
+
+ public void setWorkspaceId(String workspaceId) {
+ this.workspaceId = workspaceId;
+ }
+
+ public Long getCreated() {
+ return created;
+ }
+
+ public void setCreated(long created) {
+ this.created = created;
+ }
+
+ public Long getLastStarting() {
+ return lastStarting;
+ }
+
+ public void setLastStarting(long lastStarting) {
+ this.lastStarting = lastStarting;
+ }
+
+ public Long getLastRunning() {
+ return lastRunning;
+ }
+
+ public void setLastRunning(long lastRunning) {
+ this.lastRunning = lastRunning;
+ }
+
+ public Long getLastStopping() {
+ return lastStopping;
+ }
+
+ public void setLastStopping(long lastStopping) {
+ this.lastStopping = lastStopping;
+ }
+
+ public Long getLastStopped() {
+ return lastStopped;
+ }
+
+ public void setLastStopped(long lastStopped) {
+ this.lastStopped = lastStopped;
+ }
+
+ public Long getExpiration() {
+ return expiration;
+ }
+
+ public void setExpiration(Long expiration) {
+ this.expiration = expiration;
+ }
+
+ public WorkspaceStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(WorkspaceStatus status) {
+ this.status = status;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ WorkspaceActivity activity = (WorkspaceActivity) o;
+ return Objects.equals(workspaceId, activity.workspaceId)
+ && Objects.equals(created, activity.created)
+ && Objects.equals(lastStarting, activity.lastStarting)
+ && Objects.equals(lastRunning, activity.lastRunning)
+ && Objects.equals(lastStopping, activity.lastStopping)
+ && Objects.equals(lastStopped, activity.lastStopped)
+ && Objects.equals(expiration, activity.expiration)
+ && status == activity.status;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ workspaceId,
+ created,
+ lastStarting,
+ lastRunning,
+ lastStopping,
+ lastStopped,
+ expiration,
+ status);
+ }
+
+ @Override
+ public String toString() {
+ return "WorkspaceActivity{"
+ + "workspaceId='"
+ + workspaceId
+ + '\''
+ + ", created="
+ + created
+ + ", lastStarting="
+ + lastStarting
+ + ", lastRunning="
+ + lastRunning
+ + ", lastStopping="
+ + lastStopping
+ + ", lastStopped="
+ + lastStopped
+ + ", expiration="
+ + expiration
+ + ", status="
+ + status
+ + '}';
+ }
+}
diff --git a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityDao.java b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityDao.java
index 4fe1d45252..a1fc4b2244 100644
--- a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityDao.java
+++ b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityDao.java
@@ -12,7 +12,9 @@
package org.eclipse.che.api.workspace.activity;
import java.util.List;
+import org.eclipse.che.api.core.Page;
import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
/**
* Data access object for workspaces expiration times.
@@ -27,9 +29,21 @@ public interface WorkspaceActivityDao {
*
* @param expiration expiration object to store
* @throws ServerException when operation failed
+ * @deprecated use {@link #setExpirationTime(String, long)} instead
*/
+ @Deprecated
void setExpiration(WorkspaceExpiration expiration) throws ServerException;
+ /**
+ * Sets workspace expiration time. Any running workspace must prolong expiration time
+ * periodically, otherwise it will be stopped by passing that time.
+ *
+ * @param workspaceId the id of the workspace
+ * @param expirationTime the new expiration time
+ * @throws ServerException when operation failed
+ */
+ void setExpirationTime(String workspaceId, long expirationTime) throws ServerException;
+
/**
* Removes workspace expiration time (basically used on ws stop).
*
@@ -46,4 +60,54 @@ public interface WorkspaceActivityDao {
* @throws ServerException when operation failed
*/
List findExpired(long timestamp) throws ServerException;
+
+ /**
+ * Removes the activity record of the provided workspace.
+ *
+ * @param workspaceId the id of the workspace
+ * @throws ServerException on error
+ */
+ void removeActivity(String workspaceId) throws ServerException;
+
+ /**
+ * Sets the time a workspace has been created.
+ *
+ * @param workspaceId the id of the workspace
+ * @param createdTimestamp the time the workspace was created
+ * @throws ServerException on error
+ */
+ void setCreatedTime(String workspaceId, long createdTimestamp) throws ServerException;
+
+ /**
+ * Sets the new timestamp for the workspace entering given status.
+ *
+ * @param workspaceId the id of the transitioned workspace
+ * @param status the new workspace status
+ * @param timestamp the time the transition occurred
+ * @throws ServerException on error
+ */
+ void setStatusChangeTime(String workspaceId, WorkspaceStatus status, long timestamp)
+ throws ServerException;
+
+ /**
+ * Finds workspaces that have been in the provided status since before the provided time.
+ *
+ * @param timestamp the stop-gap time
+ * @param status the status of the workspaces
+ * @param maxItems max items on the results page
+ * @param skipCount how many items of the result to skip
+ * @return the list of workspaces ids that has the the specified status since timestamp
+ * @throws ServerException on error
+ */
+ Page findInStatusSince(
+ long timestamp, WorkspaceStatus status, int maxItems, long skipCount) throws ServerException;
+
+ /**
+ * Returns the workspace activity record of the provided workspace.
+ *
+ * @param workspaceId the id of the workspace
+ * @return the workspace activity instance
+ * @throws ServerException on error
+ */
+ WorkspaceActivity findActivity(String workspaceId) throws ServerException;
}
diff --git a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityManager.java b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityManager.java
index 04957d93a4..4c2e404a1d 100644
--- a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityManager.java
+++ b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityManager.java
@@ -23,13 +23,19 @@ import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.workspace.Workspace;
+import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.notification.EventSubscriber;
import org.eclipse.che.api.workspace.server.WorkspaceManager;
+import org.eclipse.che.api.workspace.server.event.BeforeWorkspaceRemovedEvent;
+import org.eclipse.che.api.workspace.shared.Constants;
import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent;
+import org.eclipse.che.api.workspace.shared.event.WorkspaceCreatedEvent;
import org.eclipse.che.commons.schedule.ScheduleDelay;
+import org.eclipse.che.core.db.cascade.CascadeEventSubscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,6 +52,7 @@ import org.slf4j.LoggerFactory;
*/
@Singleton
public class WorkspaceActivityManager {
+
public static final long MINIMAL_TIMEOUT = 300_000L;
private static final Logger LOG = LoggerFactory.getLogger(WorkspaceActivityManager.class);
@@ -55,7 +62,9 @@ public class WorkspaceActivityManager {
private final long defaultTimeout;
private final WorkspaceActivityDao activityDao;
private final EventService eventService;
- private final EventSubscriber> workspaceEventsSubscriber;
+ private final EventSubscriber updateStatusChangedTimestampSubscriber;
+ private final EventSubscriber setCreatedTimestampSubscriber;
+ private final EventSubscriber workspaceActivityRemover;
protected final WorkspaceManager workspaceManager;
@@ -76,36 +85,39 @@ public class WorkspaceActivityManager {
+ " minutes). This may cause problems with workspace components startup and/or premature workspace shutdown.");
}
- this.workspaceEventsSubscriber =
- new EventSubscriber() {
+ //noinspection Convert2Lambda
+ this.setCreatedTimestampSubscriber =
+ new EventSubscriber() {
@Override
- public void onEvent(WorkspaceStatusEvent event) {
- switch (event.getStatus()) {
- case RUNNING:
- try {
- Workspace workspace = workspaceManager.getWorkspace(event.getWorkspaceId());
- if (workspace.getAttributes().remove(WORKSPACE_STOPPED_BY) != null) {
- workspaceManager.updateWorkspace(event.getWorkspaceId(), workspace);
- }
- } catch (Exception ex) {
- LOG.warn(
- "Failed to remove stopped information attribute for workspace "
- + event.getWorkspaceId());
- }
- update(event.getWorkspaceId(), System.currentTimeMillis());
- break;
- case STOPPED:
- try {
- activityDao.removeExpiration(event.getWorkspaceId());
- } catch (ServerException e) {
- LOG.error(e.getLocalizedMessage(), e);
- }
- break;
- default:
- // do nothing
+ public void onEvent(WorkspaceCreatedEvent event) {
+ try {
+ long createdTime =
+ Long.parseLong(
+ event.getWorkspace().getAttributes().get(Constants.CREATED_ATTRIBUTE_NAME));
+ activityDao.setCreatedTime(event.getWorkspace().getId(), createdTime);
+ } catch (ServerException | NumberFormatException x) {
+ LOG.warn("Failed to record workspace created time in workspace activity.", x);
}
}
};
+
+ this.workspaceActivityRemover =
+ new CascadeEventSubscriber() {
+ @Override
+ public void onCascadeEvent(BeforeWorkspaceRemovedEvent event) throws Exception {
+ activityDao.removeActivity(event.getWorkspace().getId());
+ }
+ };
+
+ this.updateStatusChangedTimestampSubscriber = new UpdateStatusChangedTimestampSubscriber();
+ }
+
+ @VisibleForTesting
+ @PostConstruct
+ void subscribe() {
+ eventService.subscribe(updateStatusChangedTimestampSubscriber, WorkspaceStatusEvent.class);
+ eventService.subscribe(setCreatedTimestampSubscriber, WorkspaceCreatedEvent.class);
+ eventService.subscribe(workspaceActivityRemover, BeforeWorkspaceRemovedEvent.class);
}
/**
@@ -118,13 +130,29 @@ public class WorkspaceActivityManager {
try {
long timeout = getIdleTimeout(wsId);
if (timeout > 0) {
- activityDao.setExpiration(new WorkspaceExpiration(wsId, activityTime + timeout));
+ activityDao.setExpirationTime(wsId, activityTime + timeout);
}
} catch (ServerException e) {
LOG.error(e.getLocalizedMessage(), e);
}
}
+ /**
+ * Finds workspaces that have been in the provided status since before the provided time.
+ *
+ * @param status the status of the workspaces
+ * @param threshold the stop-gap time
+ * @param maxItems max items on the results page
+ * @param skipCount how many items of the result to skip
+ * @return the list of workspaces ids that have been in the provided status before the provided
+ * time.
+ * @throws ServerException on error
+ */
+ public Page findWorkspacesInStatus(
+ WorkspaceStatus status, long threshold, int maxItems, long skipCount) throws ServerException {
+ return activityDao.findInStatusSince(threshold, status, maxItems, skipCount);
+ }
+
protected long getIdleTimeout(String wsId) {
return defaultTimeout;
}
@@ -163,9 +191,49 @@ public class WorkspaceActivityManager {
}
}
- @VisibleForTesting
- @PostConstruct
- public void subscribe() {
- eventService.subscribe(workspaceEventsSubscriber);
+ private class UpdateStatusChangedTimestampSubscriber
+ implements EventSubscriber {
+ @Override
+ public void onEvent(WorkspaceStatusEvent event) {
+ long now = System.currentTimeMillis();
+ String workspaceId = event.getWorkspaceId();
+ WorkspaceStatus status = event.getStatus();
+
+ // first, record the activity
+ try {
+ activityDao.setStatusChangeTime(workspaceId, status, now);
+ } catch (ServerException e) {
+ LOG.warn(
+ "Failed to record workspace activity. Workspace: {}, status: {}",
+ workspaceId,
+ status.toString(),
+ e);
+ }
+
+ // now do any special handling
+ switch (status) {
+ case RUNNING:
+ try {
+ Workspace workspace = workspaceManager.getWorkspace(workspaceId);
+ if (workspace.getAttributes().remove(WORKSPACE_STOPPED_BY) != null) {
+ workspaceManager.updateWorkspace(workspaceId, workspace);
+ }
+ } catch (Exception ex) {
+ LOG.warn(
+ "Failed to remove stopped information attribute for workspace {}", workspaceId);
+ }
+ WorkspaceActivityManager.this.update(workspaceId, now);
+ break;
+ case STOPPED:
+ try {
+ activityDao.removeExpiration(workspaceId);
+ } catch (ServerException e) {
+ LOG.error(e.getLocalizedMessage(), e);
+ }
+ break;
+ default:
+ // do nothing
+ }
+ }
}
}
diff --git a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityService.java b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityService.java
index 4d752a6f1e..020aab599e 100644
--- a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityService.java
+++ b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityService.java
@@ -13,19 +13,31 @@ package org.eclipse.che.api.workspace.activity;
import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.RUNNING;
+import com.google.common.annotations.Beta;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import javax.inject.Inject;
import javax.inject.Singleton;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.Pages;
import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
import org.eclipse.che.api.core.rest.Service;
+import org.eclipse.che.api.core.rest.annotations.Required;
import org.eclipse.che.api.workspace.server.WorkspaceManager;
import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
import org.slf4j.Logger;
@@ -66,4 +78,58 @@ public class WorkspaceActivityService extends Service {
LOG.debug("Updated activity on workspace {}", wsId);
}
}
+
+ @Beta
+ @GET
+ @ApiOperation("Retrieves the IDs of workspaces that have been in given state.")
+ @ApiResponses(
+ @ApiResponse(
+ code = 200,
+ message = "Array of workspace IDs produced.",
+ response = String[].class))
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getWorkspacesByActivity(
+ @QueryParam("status") @Required @ApiParam("The requested status of the workspaces")
+ WorkspaceStatus status,
+ @QueryParam("threshold")
+ @DefaultValue("-1")
+ @ApiParam(
+ "Optionally, limit the results only to workspaces that have been in the provided"
+ + " status since before this time (in epoch millis). If both threshold and minDuration"
+ + " are specified, minDuration is NOT taken into account.")
+ long threshold,
+ @QueryParam("minDuration")
+ @DefaultValue("-1")
+ @ApiParam(
+ "Instead of a threshold, one can also use this parameter to specify the minimum"
+ + " duration that the workspaces need to have been in the given state. The duration is"
+ + " specified in milliseconds. If both threshold and minDuration are specified,"
+ + " minDuration is NOT taken into account.")
+ long minDuration,
+ @QueryParam("maxItems")
+ @DefaultValue("" + Pages.DEFAULT_PAGE_SIZE)
+ @ApiParam("Maximum number of items on a page of results.")
+ int maxItems,
+ @QueryParam("skipCount") @DefaultValue("0") @ApiParam("How many items to skip.")
+ long skipCount)
+ throws ServerException, BadRequestException {
+
+ if (status == null) {
+ throw new BadRequestException("The status query parameter is query.");
+ }
+
+ long limit = threshold;
+
+ if (limit == -1) {
+ limit = System.currentTimeMillis();
+ if (minDuration != -1) {
+ limit -= minDuration;
+ }
+ }
+
+ Page data =
+ workspaceActivityManager.findWorkspacesInStatus(status, limit, maxItems, skipCount);
+
+ return Response.ok(data).header("Link", createLinkHeader(data)).build();
+ }
}
diff --git a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceExpiration.java b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceExpiration.java
index 38f56dd559..00ad6ac23d 100644
--- a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceExpiration.java
+++ b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceExpiration.java
@@ -25,6 +25,7 @@ import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
* Data object for storing workspace expiration times.
*
* @author Max Shaposhnik (mshaposh@redhat.com)
+ * @deprecated since 6.16.0, use {@link WorkspaceActivity} instead
*/
@Entity(name = "WorkspaceExpiration")
@NamedQueries({
@@ -33,6 +34,7 @@ import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
query = "SELECT e FROM WorkspaceExpiration e WHERE e.expiration < :expiration")
})
@Table(name = "che_workspace_expiration")
+@Deprecated
public class WorkspaceExpiration {
@Id
diff --git a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/inject/WorkspaceActivityModule.java b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/inject/WorkspaceActivityModule.java
index c67d2b171e..645d93ea60 100644
--- a/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/inject/WorkspaceActivityModule.java
+++ b/wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/inject/WorkspaceActivityModule.java
@@ -13,7 +13,6 @@ package org.eclipse.che.api.workspace.activity.inject;
import com.google.inject.AbstractModule;
import org.eclipse.che.api.workspace.activity.JpaWorkspaceActivityDao;
-import org.eclipse.che.api.workspace.activity.JpaWorkspaceActivityDao.RemoveExpirationBeforeWorkspaceRemovedEventSubscriber;
import org.eclipse.che.api.workspace.activity.WorkspaceActivityDao;
import org.eclipse.che.api.workspace.activity.WorkspaceActivityManager;
import org.eclipse.che.api.workspace.activity.WorkspaceActivityService;
@@ -25,6 +24,5 @@ public class WorkspaceActivityModule extends AbstractModule {
bind(WorkspaceActivityService.class);
bind(WorkspaceActivityManager.class);
bind(WorkspaceActivityDao.class).to(JpaWorkspaceActivityDao.class);
- bind(RemoveExpirationBeforeWorkspaceRemovedEventSubscriber.class).asEagerSingleton();
}
}
diff --git a/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityManagerTest.java b/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityManagerTest.java
index 4b0f7ff8c3..2ecfbfd3ef 100644
--- a/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityManagerTest.java
+++ b/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityManagerTest.java
@@ -11,6 +11,8 @@
*/
package org.eclipse.che.api.workspace.activity;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
@@ -18,30 +20,40 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.testng.AssertJUnit.assertEquals;
+import com.google.common.collect.ImmutableMap;
+import java.util.stream.Stream;
import org.eclipse.che.account.shared.model.Account;
import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.notification.EventSubscriber;
import org.eclipse.che.api.workspace.server.WorkspaceManager;
+import org.eclipse.che.api.workspace.server.event.BeforeWorkspaceRemovedEvent;
import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
+import org.eclipse.che.api.workspace.shared.Constants;
+import org.eclipse.che.api.workspace.shared.dto.WorkspaceDto;
import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent;
+import org.eclipse.che.api.workspace.shared.event.WorkspaceCreatedEvent;
import org.eclipse.che.dto.server.DtoFactory;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
+/** Tests for {@link WorkspaceActivityManager} */
@Listeners(value = MockitoTestNGListener.class)
-/** Tests for {@link WorkspaceActivityNotifier} */
public class WorkspaceActivityManagerTest {
+
private static final long DEFAULT_TIMEOUT = 60_000L; // 1 minute
@Mock private WorkspaceManager workspaceManager;
- @Captor private ArgumentCaptor> captor;
+ @Captor private ArgumentCaptor> createEventCaptor;
+ @Captor private ArgumentCaptor> statusChangeEventCaptor;
+ @Captor private ArgumentCaptor> removeEventCaptor;
@Mock private Account account;
@Mock private WorkspaceImpl workspace;
@@ -71,23 +83,22 @@ public class WorkspaceActivityManagerTest {
activityManager.update(wsId, activityTime);
- WorkspaceExpiration expected = new WorkspaceExpiration(wsId, activityTime + DEFAULT_TIMEOUT);
- verify(workspaceActivityDao, times(1)).setExpiration(eq(expected));
+ verify(workspaceActivityDao, times(1))
+ .setExpirationTime(eq(wsId), eq(activityTime + DEFAULT_TIMEOUT));
}
@Test
public void shouldAddWorkspaceForTrackActivityWhenWorkspaceRunning() throws Exception {
final String wsId = "testWsId";
- activityManager.subscribe();
- verify(eventService).subscribe(captor.capture());
- final EventSubscriber subscriber = captor.getValue();
+ final EventSubscriber subscriber = subscribeAndGetStatusEventSubscriber();
+
subscriber.onEvent(
DtoFactory.newDto(WorkspaceStatusEvent.class)
.withStatus(WorkspaceStatus.RUNNING)
.withWorkspaceId(wsId));
- ArgumentCaptor captor = ArgumentCaptor.forClass(WorkspaceExpiration.class);
- verify(workspaceActivityDao, times(1)).setExpiration(captor.capture());
- assertEquals(captor.getValue().getWorkspaceId(), wsId);
+ ArgumentCaptor wsIdCaptor = ArgumentCaptor.forClass(String.class);
+ verify(workspaceActivityDao, times(1)).setExpirationTime(wsIdCaptor.capture(), any(long.class));
+ assertEquals(wsIdCaptor.getValue(), wsId);
}
@Test
@@ -95,9 +106,7 @@ public class WorkspaceActivityManagerTest {
final String wsId = "testWsId";
final long expiredTime = 1000L;
activityManager.update(wsId, expiredTime);
- activityManager.subscribe();
- verify(eventService).subscribe(captor.capture());
- final EventSubscriber subscriber = captor.getValue();
+ final EventSubscriber subscriber = subscribeAndGetStatusEventSubscriber();
subscriber.onEvent(
DtoFactory.newDto(WorkspaceStatusEvent.class)
@@ -106,4 +115,75 @@ public class WorkspaceActivityManagerTest {
verify(workspaceActivityDao, times(1)).removeExpiration(eq(wsId));
}
+
+ @Test
+ public void shouldRecordWorkspaceCreation() throws Exception {
+ String wsId = "1";
+
+ EventSubscriber subscriber = subscribeAndGetCreatedSubscriber();
+
+ subscriber.onEvent(
+ new WorkspaceCreatedEvent(
+ DtoFactory.newDto(WorkspaceDto.class)
+ .withId(wsId)
+ .withAttributes(ImmutableMap.of(Constants.CREATED_ATTRIBUTE_NAME, "15"))));
+
+ verify(workspaceActivityDao, times(1)).setCreatedTime(eq(wsId), eq(15L));
+ }
+
+ @Test(dataProvider = "wsStatus")
+ public void shouldRecordWorkspaceStatusChange(WorkspaceStatus status) throws Exception {
+ String wsId = "1";
+
+ EventSubscriber subscriber = subscribeAndGetStatusEventSubscriber();
+
+ subscriber.onEvent(
+ DtoFactory.newDto(WorkspaceStatusEvent.class).withStatus(status).withWorkspaceId(wsId));
+
+ verify(workspaceActivityDao, times(1)).setStatusChangeTime(eq(wsId), eq(status), anyLong());
+ }
+
+ @Test
+ public void shouldRemoveActivityWhenWorkspaceRemoved() throws Exception {
+ String wsId = "1";
+
+ EventSubscriber subscriber = subscribeAndGetRemoveSubscriber();
+
+ subscriber.onEvent(
+ new BeforeWorkspaceRemovedEvent(
+ new WorkspaceImpl(DtoFactory.newDto(WorkspaceDto.class).withId(wsId), null)));
+
+ verify(workspaceActivityDao, times(1)).removeActivity(eq(wsId));
+ }
+
+ @DataProvider(name = "wsStatus")
+ public Object[][] getWorkspaceStatus() {
+ return Stream.of(WorkspaceStatus.values())
+ .map(s -> new WorkspaceStatus[] {s})
+ .toArray(Object[][]::new);
+ }
+
+ private EventSubscriber subscribeAndGetStatusEventSubscriber() {
+ subscribeToEventService();
+ return statusChangeEventCaptor.getValue();
+ }
+
+ private EventSubscriber subscribeAndGetCreatedSubscriber() {
+ subscribeToEventService();
+ return createEventCaptor.getValue();
+ }
+
+ private EventSubscriber subscribeAndGetRemoveSubscriber() {
+ subscribeToEventService();
+ return removeEventCaptor.getValue();
+ }
+
+ private void subscribeToEventService() {
+ activityManager.subscribe();
+ verify(eventService).subscribe(createEventCaptor.capture(), eq(WorkspaceCreatedEvent.class));
+ verify(eventService)
+ .subscribe(statusChangeEventCaptor.capture(), eq(WorkspaceStatusEvent.class));
+ verify(eventService)
+ .subscribe(removeEventCaptor.capture(), eq(BeforeWorkspaceRemovedEvent.class));
+ }
}
diff --git a/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityServiceTest.java b/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityServiceTest.java
index f687ec6b0a..676f4c0eb6 100644
--- a/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityServiceTest.java
+++ b/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/WorkspaceActivityServiceTest.java
@@ -12,16 +12,23 @@
package org.eclipse.che.api.workspace.activity;
import static com.jayway.restassured.RestAssured.given;
+import static java.util.Collections.emptyList;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import com.jayway.restassured.response.Response;
+import java.net.URI;
import org.eclipse.che.account.spi.AccountImpl;
import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.Pages;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
import org.eclipse.che.api.core.rest.ApiExceptionMapper;
@@ -64,6 +71,7 @@ public class WorkspaceActivityServiceTest {
@Mock private WorkspaceManager workspaceManager;
+ @SuppressWarnings({"FieldCanBeLocal", "unused"})
private WorkspaceActivityService workspaceActivityService;
@BeforeMethod
@@ -100,6 +108,45 @@ public class WorkspaceActivityServiceTest {
verifyZeroInteractions(workspaceActivityManager);
}
+ @Test
+ public void shouldRequireStatusParameterForActivityQueries() {
+ Response response = given().when().get(URI.create(SERVICE_PATH));
+
+ assertEquals(response.getStatusCode(), 400);
+ verifyZeroInteractions(workspaceActivityManager);
+ }
+
+ @Test
+ public void shouldBeAbleToQueryWithoutTimeConstraints() throws ServerException {
+ Page emptyPage = new Page<>(emptyList(), 0, 1, 0);
+ when(workspaceActivityManager.findWorkspacesInStatus(any(), anyLong(), anyInt(), anyLong()))
+ .thenReturn(emptyPage);
+
+ Response response = given().when().get(URI.create(SERVICE_PATH + "?status=RUNNING"));
+
+ assertEquals(response.getStatusCode(), 200);
+ verify(workspaceActivityManager, times(1))
+ .findWorkspacesInStatus(
+ eq(WorkspaceStatus.RUNNING), anyLong(), eq(Pages.DEFAULT_PAGE_SIZE), eq(0L));
+ }
+
+ @Test
+ public void shouldIgnoredMinDurationWhenThresholdSpecified() throws Exception {
+ when(workspaceActivityManager.findWorkspacesInStatus(
+ eq(WorkspaceStatus.STOPPED), anyLong(), anyInt(), anyLong()))
+ .thenReturn(new Page<>(emptyList(), 0, 1, 0));
+
+ Response response =
+ given()
+ .when()
+ .get(URI.create(SERVICE_PATH + "?status=STOPPED&threshold=15&minDuration=55"));
+
+ assertEquals(response.getStatusCode(), 200);
+ verify(workspaceActivityManager, times(1))
+ .findWorkspacesInStatus(
+ eq(WorkspaceStatus.STOPPED), eq(15L), eq(Pages.DEFAULT_PAGE_SIZE), eq(0L));
+ }
+
@DataProvider(name = "wsStatus")
public Object[][] getWorkspaceStatus() {
return new Object[][] {
@@ -109,6 +156,7 @@ public class WorkspaceActivityServiceTest {
@Filter
public static class EnvironmentFilter implements RequestFilter {
+
@Override
public void doFilter(GenericContainerRequest request) {
EnvironmentContext.getCurrent().setSubject(TEST_USER);
diff --git a/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/jpa/WorkspaceActivityTckModule.java b/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/jpa/WorkspaceActivityTckModule.java
index 2e788b8cdc..5b420fd78f 100644
--- a/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/jpa/WorkspaceActivityTckModule.java
+++ b/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/jpa/WorkspaceActivityTckModule.java
@@ -16,8 +16,8 @@ import java.sql.Driver;
import java.util.Collection;
import org.eclipse.che.account.spi.AccountImpl;
import org.eclipse.che.api.workspace.activity.JpaWorkspaceActivityDao;
+import org.eclipse.che.api.workspace.activity.WorkspaceActivity;
import org.eclipse.che.api.workspace.activity.WorkspaceActivityDao;
-import org.eclipse.che.api.workspace.activity.WorkspaceExpiration;
import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl;
import org.eclipse.che.api.workspace.server.model.impl.MachineConfigImpl;
@@ -52,7 +52,7 @@ public class WorkspaceActivityTckModule extends TckModule {
.setDriver(Driver.class)
.runningOn(server)
.addEntityClasses(
- WorkspaceExpiration.class,
+ WorkspaceActivity.class,
AccountImpl.class,
WorkspaceImpl.class,
WorkspaceConfigImpl.class,
@@ -79,8 +79,8 @@ public class WorkspaceActivityTckModule extends TckModule {
bind(new TypeLiteral>() {})
.toInstance(new JpaTckRepository<>(AccountImpl.class));
- bind(new TypeLiteral>() {})
- .toInstance(new JpaTckRepository<>(WorkspaceExpiration.class));
+ bind(new TypeLiteral>() {})
+ .toInstance(new JpaTckRepository<>(WorkspaceActivity.class));
bind(new TypeLiteral>() {}).toInstance(new WorkspaceRepository());
}
diff --git a/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/spi/tck/WorkspaceActivityDaoTest.java b/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/spi/tck/WorkspaceActivityDaoTest.java
index 7d6275443f..c4cedc93a6 100644
--- a/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/spi/tck/WorkspaceActivityDaoTest.java
+++ b/wsmaster/che-core-api-workspace-activity/src/test/java/org/eclipse/che/api/workspace/activity/spi/tck/WorkspaceActivityDaoTest.java
@@ -12,21 +12,25 @@
package org.eclipse.che.api.workspace.activity.spi.tck;
import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
+import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STARTING;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.stream.Stream;
import javax.inject.Inject;
import org.eclipse.che.account.spi.AccountImpl;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
+import org.eclipse.che.api.workspace.activity.WorkspaceActivity;
import org.eclipse.che.api.workspace.activity.WorkspaceActivityDao;
-import org.eclipse.che.api.workspace.activity.WorkspaceExpiration;
import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl;
import org.eclipse.che.api.workspace.server.model.impl.MachineConfigImpl;
@@ -42,6 +46,7 @@ import org.eclipse.che.commons.test.tck.repository.TckRepository;
import org.eclipse.che.commons.test.tck.repository.TckRepositoryException;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@@ -58,11 +63,11 @@ public class WorkspaceActivityDaoTest {
private AccountImpl[] accounts = new AccountImpl[COUNT];
private WorkspaceImpl[] workspaces = new WorkspaceImpl[COUNT];
- private WorkspaceExpiration[] expirations = new WorkspaceExpiration[COUNT];
+ private WorkspaceActivity[] activities = new WorkspaceActivity[COUNT];
@Inject private TckRepository accountTckRepository;
- @Inject private TckRepository expirationTckRepository;
+ @Inject private TckRepository activityTckRepository;
@Inject private TckRepository wsTckRepository;
@@ -74,25 +79,34 @@ public class WorkspaceActivityDaoTest {
// 2 workspaces share 1 namespace
workspaces[i] = createWorkspace("ws" + i, accounts[i / 2], "name-" + i);
- expirations[i] = new WorkspaceExpiration("ws" + i, (i + 1) * 1_000_000);
+ long base = (long) i + 1;
+ WorkspaceActivity a = new WorkspaceActivity();
+ a.setWorkspaceId("ws" + i);
+ a.setCreated(base);
+ a.setLastStarting(base * 10);
+ a.setLastRunning(base * 100);
+ a.setLastStopping(base * 1_000);
+ a.setLastStopped(base * 10_000);
+ a.setExpiration(base * 1_000_000);
+
+ activities[i] = a;
}
accountTckRepository.createAll(asList(accounts));
wsTckRepository.createAll(asList(workspaces));
- expirationTckRepository.createAll(asList(expirations));
+ activityTckRepository.createAll(asList(activities));
}
@AfterMethod
private void cleanup() throws TckRepositoryException {
- expirationTckRepository.removeAll();
+ activityTckRepository.removeAll();
wsTckRepository.removeAll();
accountTckRepository.removeAll();
}
@Test
public void shouldFindExpirationsByTimestamp() throws Exception {
- List expected =
- Arrays.asList(expirations[0].getWorkspaceId(), expirations[1].getWorkspaceId());
+ List expected = asList(activities[0].getWorkspaceId(), activities[1].getWorkspaceId());
List found = workspaceActivityDao.findExpired(2_500_000);
assertEquals(found, expected);
@@ -100,9 +114,9 @@ public class WorkspaceActivityDaoTest {
@Test(dependsOnMethods = "shouldFindExpirationsByTimestamp")
public void shouldRemoveExpirationsByWsId() throws Exception {
- List expected = Collections.singletonList(expirations[1].getWorkspaceId());
+ List expected = singletonList(activities[1].getWorkspaceId());
- workspaceActivityDao.removeExpiration(expirations[0].getWorkspaceId());
+ workspaceActivityDao.removeExpiration(activities[0].getWorkspaceId());
List found = workspaceActivityDao.findExpired(2_500_000);
assertEquals(found, expected);
@@ -112,12 +126,11 @@ public class WorkspaceActivityDaoTest {
public void shouldUpdateExpirations() throws Exception {
List expected =
- Arrays.asList(
- expirations[0].getWorkspaceId(),
- expirations[2].getWorkspaceId(),
- expirations[1].getWorkspaceId());
- workspaceActivityDao.setExpiration(
- new WorkspaceExpiration(expirations[2].getWorkspaceId(), 1_750_000));
+ asList(
+ activities[0].getWorkspaceId(),
+ activities[2].getWorkspaceId(),
+ activities[1].getWorkspaceId());
+ workspaceActivityDao.setExpirationTime(activities[2].getWorkspaceId(), 1_750_000);
List found = workspaceActivityDao.findExpired(2_500_000);
assertEquals(found, expected);
@@ -126,18 +139,60 @@ public class WorkspaceActivityDaoTest {
@Test(dependsOnMethods = {"shouldFindExpirationsByTimestamp", "shouldRemoveExpirationsByWsId"})
public void shouldAddExpirations() throws Exception {
- List expected =
- Arrays.asList(expirations[0].getWorkspaceId(), expirations[1].getWorkspaceId());
- workspaceActivityDao.removeExpiration(expirations[1].getWorkspaceId());
+ List expected = asList(activities[0].getWorkspaceId(), activities[1].getWorkspaceId());
+ workspaceActivityDao.removeExpiration(activities[1].getWorkspaceId());
// create new again
- workspaceActivityDao.setExpiration(
- new WorkspaceExpiration(expirations[1].getWorkspaceId(), 1_250_000));
+ workspaceActivityDao.setExpirationTime(activities[1].getWorkspaceId(), 1_250_000);
List found = workspaceActivityDao.findExpired(1_500_000);
assertEquals(found, expected);
}
+ @Test
+ public void shouldNotCareAboutCreatedAndStatusChangeOrder() throws Exception {
+ Page found =
+ workspaceActivityDao.findInStatusSince(System.currentTimeMillis(), STARTING, 1000, 0);
+
+ assertTrue(found.isEmpty());
+
+ workspaceActivityDao.setCreatedTime(activities[0].getWorkspaceId(), 1L);
+ workspaceActivityDao.setStatusChangeTime(activities[0].getWorkspaceId(), STARTING, 2L);
+
+ workspaceActivityDao.setStatusChangeTime(activities[1].getWorkspaceId(), STARTING, 2L);
+ workspaceActivityDao.setCreatedTime(activities[1].getWorkspaceId(), 1L);
+
+ found = workspaceActivityDao.findInStatusSince(System.currentTimeMillis(), STARTING, 1000, 0);
+
+ assertEquals(
+ found.getItems(), asList(activities[0].getWorkspaceId(), activities[1].getWorkspaceId()));
+ }
+
+ @Test(dataProvider = "allWorkspaceStatuses")
+ public void shouldFindActivityByLastStatusChangeTime(WorkspaceStatus status) throws Exception {
+ Page found =
+ workspaceActivityDao.findInStatusSince(System.currentTimeMillis(), status, 1000, 0);
+
+ assertTrue(found.isEmpty());
+
+ workspaceActivityDao.setCreatedTime(activities[0].getWorkspaceId(), 1L);
+ workspaceActivityDao.setStatusChangeTime(activities[0].getWorkspaceId(), status, 2L);
+
+ workspaceActivityDao.setStatusChangeTime(activities[1].getWorkspaceId(), status, 5L);
+ workspaceActivityDao.setCreatedTime(activities[1].getWorkspaceId(), 1L);
+
+ found = workspaceActivityDao.findInStatusSince(3L, status, 1000, 0);
+
+ assertEquals(found.getItems(), singletonList(activities[0].getWorkspaceId()));
+ }
+
+ @DataProvider(name = "allWorkspaceStatuses")
+ public Object[][] getWorkspaceStatus() {
+ return Stream.of(WorkspaceStatus.values())
+ .map(s -> new WorkspaceStatus[] {s})
+ .toArray(Object[][]::new);
+ }
+
private static WorkspaceConfigImpl createWorkspaceConfig(String name) {
// Project Sources configuration
final SourceStorageImpl source1 = new SourceStorageImpl();
@@ -153,11 +208,11 @@ public class WorkspaceActivityDaoTest {
pCfg1.getMixins().addAll(asList("mixin1", "mixin2"));
pCfg1.setSource(source1);
- final List projects = new ArrayList<>(Collections.singletonList(pCfg1));
+ final List projects = new ArrayList<>(singletonList(pCfg1));
// Commands
final CommandImpl cmd1 = new CommandImpl("name1", "cmd1", "type1");
- final List commands = new ArrayList<>(Collections.singletonList(cmd1));
+ final List commands = new ArrayList<>(singletonList(cmd1));
// OldMachine configs
final MachineConfigImpl exMachine1 = new MachineConfigImpl();
diff --git a/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/2__create_workspace_activity_table.sql b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/2__create_workspace_activity_table.sql
new file mode 100644
index 0000000000..75cb7a6606
--- /dev/null
+++ b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/2__create_workspace_activity_table.sql
@@ -0,0 +1,30 @@
+--
+-- 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
+--
+
+CREATE TABLE che_workspace_activity (
+ workspace_id VARCHAR(255) NOT NULL,
+ status VARCHAR(255),
+ created BIGINT,
+ last_starting BIGINT,
+ last_running BIGINT,
+ last_stopping BIGINT,
+ last_stopped BIGINT,
+ expiration BIGINT,
+
+ PRIMARY KEY (workspace_id)
+);
+ALTER TABLE che_workspace_activity ADD CONSTRAINT ws_activity_workspace_id FOREIGN KEY (workspace_id) REFERENCES workspace (id);
+CREATE INDEX che_index_ws_activity_last_starting ON che_workspace_activity (status, last_starting);
+CREATE INDEX che_index_ws_activity_last_running ON che_workspace_activity (status, last_running);
+CREATE INDEX che_index_ws_activity_last_stopping ON che_workspace_activity (status, last_stopping);
+CREATE INDEX che_index_ws_activity_last_stopped ON che_workspace_activity (status, last_stopped);
+CREATE INDEX che_index_ws_activity_expiration ON che_workspace_activity (expiration);
diff --git a/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/3__bootstrap_ws_activity_data.sql b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/3__bootstrap_ws_activity_data.sql
new file mode 100644
index 0000000000..6abf919487
--- /dev/null
+++ b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/3__bootstrap_ws_activity_data.sql
@@ -0,0 +1,46 @@
+--
+-- 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
+--
+
+-- copy data from the old workspace expiration table. Leave the old table and data intact, we just
+-- won't be using it anymore, but leave it there so that we don't make breaking schema changes
+-- straight away.
+
+-- We specialize for postgres and mysql, leaving this default for H2.
+
+INSERT INTO che_workspace_activity (workspace_id, created, expiration, status, last_running, last_stopped)
+ SELECT a.workspace_id, cast(a.attributes as bigint), e.expiration,
+ CASE
+ WHEN r.status = '0' THEN 'STARTING'
+ WHEN r.status = '1' THEN 'RUNNING'
+ WHEN r.status = '2' THEN 'STOPPING'
+ ELSE 'STOPPED' -- also handles the lack of explicit status
+ END,
+ cast(a_forRunning.attributes as bigint), cast(a_forStopped.attributes as bigint)
+ FROM workspace_attributes AS a
+ -- pull in the existing expiration times
+ LEFT JOIN che_workspace_expiration AS e
+ ON a.workspace_id = e.workspace_id
+ -- pull in the recorded current status of the workspaces
+ LEFT JOIN che_k8s_runtime AS r
+ ON a.workspace_id = r.workspace_id
+ -- consider the 'updated' time of a running workspace as its "last_running" event time
+ LEFT JOIN workspace_attributes AS a_forRunning
+ ON a.workspace_id = a_forRunning.workspace_id
+ AND r.status = '1'
+ AND a_forRunning.attributes_key = 'updated'
+ -- pick up the 'stopped' timestamp from the workspace attributes (if any)
+ LEFT JOIN workspace_attributes AS a_forStopped
+ ON a.workspace_id = a_forStopped.workspace_id
+ AND a_forStopped.attributes_key = 'stopped'
+ -- we're basing all of the above on workspaces that have the 'created' attribute that stores the
+ -- timestamp when the workspace was created
+ WHERE a.attributes_key = 'created';
diff --git a/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/mysql/3__bootstrap_ws_activity_data.sql b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/mysql/3__bootstrap_ws_activity_data.sql
new file mode 100644
index 0000000000..a6d5fe236a
--- /dev/null
+++ b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/mysql/3__bootstrap_ws_activity_data.sql
@@ -0,0 +1,44 @@
+--
+-- 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
+--
+
+-- copy data from the old workspace expiration table. Leave the old table and data intact, we just
+-- won't be using it anymore, but leave it there so that we don't make breaking schema changes
+-- straight away.
+
+INSERT INTO che_workspace_activity (workspace_id, created, expiration, status, last_running, last_stopped)
+ SELECT a.workspace_id, a.attributes, e.expiration,
+ CASE
+ WHEN r.status = '0' THEN 'STARTING'
+ WHEN r.status = '1' THEN 'RUNNING'
+ WHEN r.status = '2' THEN 'STOPPING'
+ ELSE 'STOPPED' -- also handles the lack of explicit status
+ END,
+ a_forRunning.attributes, a_forStopped.attributes
+ FROM workspace_attributes AS a
+ -- pull in the existing expiration times
+ LEFT JOIN che_workspace_expiration AS e
+ ON a.workspace_id = e.workspace_id
+ -- pull in the recorded current status of the workspaces
+ LEFT JOIN che_k8s_runtime AS r
+ ON a.workspace_id = r.workspace_id
+ -- consider the 'updated' time of a running workspace as its "last_running" event time
+ LEFT JOIN workspace_attributes AS a_forRunning
+ ON a.workspace_id = a_forRunning.workspace_id
+ AND r.status = 'RUNNING'
+ AND a_forRunning.attributes_key = 'updated'
+ -- pick up the 'stopped' timestamp from the workspace attributes (if any)
+ LEFT JOIN workspace_attributes AS a_forStopped
+ ON a.workspace_id = a_forStopped.workspace_id
+ AND a_forStopped.attributes_key = 'stopped'
+ -- we're basing all of the above on workspaces that have the 'created' attribute that stores the
+ -- timestamp when the workspace was created
+ WHERE a.attributes_key = 'created';
diff --git a/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/CascadeRemovalTest.java b/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/CascadeRemovalTest.java
index 79440eac55..2e0779a7ec 100644
--- a/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/CascadeRemovalTest.java
+++ b/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/CascadeRemovalTest.java
@@ -61,8 +61,8 @@ import org.eclipse.che.api.user.server.model.impl.UserImpl;
import org.eclipse.che.api.user.server.spi.PreferenceDao;
import org.eclipse.che.api.user.server.spi.ProfileDao;
import org.eclipse.che.api.user.server.spi.UserDao;
+import org.eclipse.che.api.workspace.activity.WorkspaceActivity;
import org.eclipse.che.api.workspace.activity.WorkspaceActivityDao;
-import org.eclipse.che.api.workspace.activity.WorkspaceExpiration;
import org.eclipse.che.api.workspace.activity.inject.WorkspaceActivityModule;
import org.eclipse.che.api.workspace.server.DefaultWorkspaceLockService;
import org.eclipse.che.api.workspace.server.DefaultWorkspaceStatusCache;
@@ -174,7 +174,7 @@ public class CascadeRemovalTest {
PreferenceEntity.class,
WorkspaceImpl.class,
WorkspaceConfigImpl.class,
- WorkspaceExpiration.class,
+ WorkspaceActivity.class,
ProjectConfigImpl.class,
EnvironmentImpl.class,
MachineConfigImpl.class,
@@ -277,6 +277,8 @@ public class CascadeRemovalTest {
assertTrue(preferenceDao.getPreferences(user.getId()).isEmpty());
assertTrue(sshDao.get(user.getId()).isEmpty());
assertTrue(workspaceDao.getByNamespace(user.getName(), 30, 0).isEmpty());
+ assertNull(notFoundToNull(() -> workspaceActivityDao.findActivity(workspace1.getId())));
+ assertNull(notFoundToNull(() -> workspaceActivityDao.findActivity(workspace2.getId())));
}
@Test(dataProvider = "beforeUserRemoveRollbackActions")
@@ -346,10 +348,10 @@ public class CascadeRemovalTest {
workspaceDao.create(workspace1 = createWorkspace("workspace1", account));
workspaceDao.create(workspace2 = createWorkspace("workspace2", account));
- workspaceActivityDao.setExpiration(
- new WorkspaceExpiration(workspace1.getId(), System.currentTimeMillis()));
- workspaceActivityDao.setExpiration(
- new WorkspaceExpiration(workspace2.getId(), System.currentTimeMillis()));
+ workspaceActivityDao.setCreatedTime(workspace1.getId(), System.currentTimeMillis());
+ workspaceActivityDao.setCreatedTime(workspace2.getId(), System.currentTimeMillis());
+ workspaceActivityDao.setExpirationTime(workspace1.getId(), System.currentTimeMillis());
+ workspaceActivityDao.setExpirationTime(workspace2.getId(), System.currentTimeMillis());
sshDao.create(sshPair1 = createSshPair(user.getId(), "service", "name1"));
sshDao.create(sshPair2 = createSshPair(user.getId(), "service", "name2"));
@@ -364,8 +366,8 @@ public class CascadeRemovalTest {
sshDao.remove(sshPair1.getOwner(), sshPair1.getService(), sshPair1.getName());
sshDao.remove(sshPair2.getOwner(), sshPair2.getService(), sshPair2.getName());
- workspaceActivityDao.removeExpiration(workspace1.getId());
- workspaceActivityDao.removeExpiration(workspace2.getId());
+ workspaceActivityDao.removeActivity(workspace1.getId());
+ workspaceActivityDao.removeActivity(workspace2.getId());
k8sMachines.remove(k8sRuntimeState.getRuntimeId());
k8sRuntimes.remove(k8sRuntimeState.getRuntimeId());
diff --git a/wsmaster/integration-tests/mysql-tck/pom.xml b/wsmaster/integration-tests/mysql-tck/pom.xml
index 0fe9f33c07..3fd02d7561 100644
--- a/wsmaster/integration-tests/mysql-tck/pom.xml
+++ b/wsmaster/integration-tests/mysql-tck/pom.xml
@@ -303,7 +303,11 @@
jdbc.port:3306
- ready for connections
+
+
+ 3306
+
+
diff --git a/wsmaster/integration-tests/mysql-tck/src/test/java/MySqlTckModule.java b/wsmaster/integration-tests/mysql-tck/src/test/java/MySqlTckModule.java
index 2e23811876..de0c4df9dd 100644
--- a/wsmaster/integration-tests/mysql-tck/src/test/java/MySqlTckModule.java
+++ b/wsmaster/integration-tests/mysql-tck/src/test/java/MySqlTckModule.java
@@ -41,6 +41,7 @@ import org.eclipse.che.api.user.server.spi.PreferenceDao;
import org.eclipse.che.api.user.server.spi.ProfileDao;
import org.eclipse.che.api.user.server.spi.UserDao;
import org.eclipse.che.api.workspace.activity.JpaWorkspaceActivityDao;
+import org.eclipse.che.api.workspace.activity.WorkspaceActivity;
import org.eclipse.che.api.workspace.activity.WorkspaceActivityDao;
import org.eclipse.che.api.workspace.activity.WorkspaceExpiration;
import org.eclipse.che.api.workspace.server.jpa.JpaStackDao;
@@ -131,7 +132,7 @@ public class MySqlTckModule extends TckModule {
SshPairImpl.class,
InstallerImpl.class,
InstallerServerConfigImpl.class,
- WorkspaceExpiration.class,
+ WorkspaceActivity.class,
VolumeImpl.class,
SignatureKeyImpl.class,
SignatureKeyPairImpl.class,
diff --git a/wsmaster/integration-tests/postgresql-tck/src/test/java/PostgreSqlTckModule.java b/wsmaster/integration-tests/postgresql-tck/src/test/java/PostgreSqlTckModule.java
index a46c440366..de88406dd2 100644
--- a/wsmaster/integration-tests/postgresql-tck/src/test/java/PostgreSqlTckModule.java
+++ b/wsmaster/integration-tests/postgresql-tck/src/test/java/PostgreSqlTckModule.java
@@ -41,6 +41,7 @@ import org.eclipse.che.api.user.server.spi.PreferenceDao;
import org.eclipse.che.api.user.server.spi.ProfileDao;
import org.eclipse.che.api.user.server.spi.UserDao;
import org.eclipse.che.api.workspace.activity.JpaWorkspaceActivityDao;
+import org.eclipse.che.api.workspace.activity.WorkspaceActivity;
import org.eclipse.che.api.workspace.activity.WorkspaceActivityDao;
import org.eclipse.che.api.workspace.activity.WorkspaceExpiration;
import org.eclipse.che.api.workspace.server.jpa.JpaStackDao;
@@ -128,7 +129,7 @@ public class PostgreSqlTckModule extends TckModule {
SshPairImpl.class,
InstallerImpl.class,
InstallerServerConfigImpl.class,
- WorkspaceExpiration.class,
+ WorkspaceActivity.class,
VolumeImpl.class,
// k8s-runtimes
KubernetesRuntimeState.class,
@@ -283,6 +284,7 @@ public class PostgreSqlTckModule extends TckModule {
}
private static class WorkspaceRepository extends JpaTckRepository {
+
public WorkspaceRepository() {
super(WorkspaceImpl.class);
}
@@ -298,6 +300,7 @@ public class PostgreSqlTckModule extends TckModule {
}
private static class StackRepository extends JpaTckRepository {
+
public StackRepository() {
super(StackImpl.class);
}