From 90903645eed5a3d02ef6eaa41a3e010f6ffcf150 Mon Sep 17 00:00:00 2001 From: Lukas Krejci Date: Sat, 22 Dec 2018 16:19:38 +0100 Subject: [PATCH] Add an ability to get workspaces ids by status and threshold timestamp (#12177) It includes: - reworking Workspace Activity API to store timestamps of each status changes; - add REST API endpoint to get workspaces ids by status and threshold timestamp; --- .../main/resources/META-INF/persistence.xml | 1 + .../api/permission/server/SystemDomain.java | 9 +- ...MultiUserWorkspaceActivityManagerTest.java | 9 +- .../che-multiuser-mysql-tck/pom.xml | 6 +- .../pom.xml | 8 + .../activity/ActivityPermissionsFilter.java | 20 +- .../ActivityPermissionsFilterTest.java | 51 ++++ .../InmemoryWorkspaceActivityDao.java | 108 ++++++++- .../activity/JpaWorkspaceActivityDao.java | 217 +++++++++++++---- .../workspace/activity/WorkspaceActivity.java | 226 ++++++++++++++++++ .../activity/WorkspaceActivityDao.java | 64 +++++ .../activity/WorkspaceActivityManager.java | 132 +++++++--- .../activity/WorkspaceActivityService.java | 66 +++++ .../activity/WorkspaceExpiration.java | 2 + .../inject/WorkspaceActivityModule.java | 2 - .../WorkspaceActivityManagerTest.java | 106 +++++++- .../WorkspaceActivityServiceTest.java | 48 ++++ .../jpa/WorkspaceActivityTckModule.java | 8 +- .../spi/tck/WorkspaceActivityDaoTest.java | 105 ++++++-- .../2__create_workspace_activity_table.sql | 30 +++ .../6.16.0/3__bootstrap_ws_activity_data.sql | 46 ++++ .../mysql/3__bootstrap_ws_activity_data.sql | 44 ++++ .../che/core/db/jpa/CascadeRemovalTest.java | 18 +- wsmaster/integration-tests/mysql-tck/pom.xml | 6 +- .../src/test/java/MySqlTckModule.java | 3 +- .../src/test/java/PostgreSqlTckModule.java | 5 +- 26 files changed, 1176 insertions(+), 164 deletions(-) create mode 100644 wsmaster/che-core-api-workspace-activity/src/main/java/org/eclipse/che/api/workspace/activity/WorkspaceActivity.java create mode 100644 wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/2__create_workspace_activity_table.sql create mode 100644 wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/3__bootstrap_ws_activity_data.sql create mode 100644 wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.16.0/mysql/3__bootstrap_ws_activity_data.sql 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); }