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;6.19.x
parent
69288516c8
commit
90903645ee
|
|
@ -66,6 +66,7 @@
|
|||
<class>org.eclipse.che.multiuser.organization.spi.impl.OrganizationDistributedResourcesImpl</class>
|
||||
|
||||
<class>org.eclipse.che.api.workspace.activity.WorkspaceExpiration</class>
|
||||
<class>org.eclipse.che.api.workspace.activity.WorkspaceActivity</class>
|
||||
<class>org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyImpl</class>
|
||||
<class>org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl</class>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SystemPermissionsImp
|
|||
public static final String SYSTEM_DOMAIN_ACTIONS = "system.domain.actions";
|
||||
public static final String DOMAIN_ID = "system";
|
||||
public static final String MANAGE_SYSTEM_ACTION = "manageSystem";
|
||||
public static final String MONITOR_SYSTEM_ACTION = "monitorSystem";
|
||||
|
||||
@Inject
|
||||
public SystemDomain(@Named(SYSTEM_DOMAIN_ACTIONS) Set<String> 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,7 +252,11 @@
|
|||
<port>jdbc.port:3306</port>
|
||||
</ports>
|
||||
<wait>
|
||||
<log>ready for connections</log>
|
||||
<tcp>
|
||||
<ports>
|
||||
<port>3306</port>
|
||||
</ports>
|
||||
</tcp>
|
||||
<time>60000</time>
|
||||
</wait>
|
||||
<restartPolicy>
|
||||
|
|
|
|||
|
|
@ -30,10 +30,18 @@
|
|||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-model</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-commons-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.multiuser</groupId>
|
||||
<artifactId>che-multiuser-api-permission</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.che.multiuser</groupId>
|
||||
<artifactId>che-multiuser-permission-workspace</artifactId>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>(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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, Long> activeWorkspaces = new ConcurrentHashMap<>();
|
||||
private final Map<String, WorkspaceActivity> 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<String> 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<String> findInStatusSince(
|
||||
long timestamp, WorkspaceStatus status, int maxItems, long skipCount) {
|
||||
List<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> 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<String> 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<String> 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<WorkspaceActivity> 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<String> 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<String> 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<BeforeWorkspaceRemovedEvent> {
|
||||
|
||||
@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<WorkspaceActivity> updater)
|
||||
throws ServerException {
|
||||
doUpdate(false, workspaceId, updater);
|
||||
}
|
||||
|
||||
@Transactional(rollbackOn = ServerException.class)
|
||||
protected void doUpdateOptionally(String workspaceId, Consumer<WorkspaceActivity> updater)
|
||||
throws ServerException {
|
||||
doUpdate(true, workspaceId, updater);
|
||||
}
|
||||
|
||||
private void doUpdate(boolean optional, String workspaceId, Consumer<WorkspaceActivity> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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<String> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceStatusEvent> updateStatusChangedTimestampSubscriber;
|
||||
private final EventSubscriber<WorkspaceCreatedEvent> setCreatedTimestampSubscriber;
|
||||
private final EventSubscriber<BeforeWorkspaceRemovedEvent> 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<WorkspaceStatusEvent>() {
|
||||
//noinspection Convert2Lambda
|
||||
this.setCreatedTimestampSubscriber =
|
||||
new EventSubscriber<WorkspaceCreatedEvent>() {
|
||||
@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<BeforeWorkspaceRemovedEvent>() {
|
||||
@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<String> 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<WorkspaceStatusEvent> {
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> data =
|
||||
workspaceActivityManager.findWorkspacesInStatus(status, limit, maxItems, skipCount);
|
||||
|
||||
return Response.ok(data).header("Link", createLinkHeader(data)).build();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EventSubscriber<WorkspaceStatusEvent>> captor;
|
||||
@Captor private ArgumentCaptor<EventSubscriber<WorkspaceCreatedEvent>> createEventCaptor;
|
||||
@Captor private ArgumentCaptor<EventSubscriber<WorkspaceStatusEvent>> statusChangeEventCaptor;
|
||||
@Captor private ArgumentCaptor<EventSubscriber<BeforeWorkspaceRemovedEvent>> 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<WorkspaceStatusEvent> subscriber = captor.getValue();
|
||||
final EventSubscriber<WorkspaceStatusEvent> subscriber = subscribeAndGetStatusEventSubscriber();
|
||||
|
||||
subscriber.onEvent(
|
||||
DtoFactory.newDto(WorkspaceStatusEvent.class)
|
||||
.withStatus(WorkspaceStatus.RUNNING)
|
||||
.withWorkspaceId(wsId));
|
||||
ArgumentCaptor<WorkspaceExpiration> captor = ArgumentCaptor.forClass(WorkspaceExpiration.class);
|
||||
verify(workspaceActivityDao, times(1)).setExpiration(captor.capture());
|
||||
assertEquals(captor.getValue().getWorkspaceId(), wsId);
|
||||
ArgumentCaptor<String> 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<WorkspaceStatusEvent> subscriber = captor.getValue();
|
||||
final EventSubscriber<WorkspaceStatusEvent> 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<WorkspaceCreatedEvent> 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<WorkspaceStatusEvent> 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<BeforeWorkspaceRemovedEvent> 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<WorkspaceStatusEvent> subscribeAndGetStatusEventSubscriber() {
|
||||
subscribeToEventService();
|
||||
return statusChangeEventCaptor.getValue();
|
||||
}
|
||||
|
||||
private EventSubscriber<WorkspaceCreatedEvent> subscribeAndGetCreatedSubscriber() {
|
||||
subscribeToEventService();
|
||||
return createEventCaptor.getValue();
|
||||
}
|
||||
|
||||
private EventSubscriber<BeforeWorkspaceRemovedEvent> 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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> 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);
|
||||
|
|
|
|||
|
|
@ -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<TckRepository<AccountImpl>>() {})
|
||||
.toInstance(new JpaTckRepository<>(AccountImpl.class));
|
||||
|
||||
bind(new TypeLiteral<TckRepository<WorkspaceExpiration>>() {})
|
||||
.toInstance(new JpaTckRepository<>(WorkspaceExpiration.class));
|
||||
bind(new TypeLiteral<TckRepository<WorkspaceActivity>>() {})
|
||||
.toInstance(new JpaTckRepository<>(WorkspaceActivity.class));
|
||||
|
||||
bind(new TypeLiteral<TckRepository<WorkspaceImpl>>() {}).toInstance(new WorkspaceRepository());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AccountImpl> accountTckRepository;
|
||||
|
||||
@Inject private TckRepository<WorkspaceExpiration> expirationTckRepository;
|
||||
@Inject private TckRepository<WorkspaceActivity> activityTckRepository;
|
||||
|
||||
@Inject private TckRepository<WorkspaceImpl> 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<String> expected =
|
||||
Arrays.asList(expirations[0].getWorkspaceId(), expirations[1].getWorkspaceId());
|
||||
List<String> expected = asList(activities[0].getWorkspaceId(), activities[1].getWorkspaceId());
|
||||
List<String> 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<String> expected = Collections.singletonList(expirations[1].getWorkspaceId());
|
||||
List<String> expected = singletonList(activities[1].getWorkspaceId());
|
||||
|
||||
workspaceActivityDao.removeExpiration(expirations[0].getWorkspaceId());
|
||||
workspaceActivityDao.removeExpiration(activities[0].getWorkspaceId());
|
||||
|
||||
List<String> found = workspaceActivityDao.findExpired(2_500_000);
|
||||
assertEquals(found, expected);
|
||||
|
|
@ -112,12 +126,11 @@ public class WorkspaceActivityDaoTest {
|
|||
public void shouldUpdateExpirations() throws Exception {
|
||||
|
||||
List<String> 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<String> 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<String> expected =
|
||||
Arrays.asList(expirations[0].getWorkspaceId(), expirations[1].getWorkspaceId());
|
||||
workspaceActivityDao.removeExpiration(expirations[1].getWorkspaceId());
|
||||
List<String> 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<String> found = workspaceActivityDao.findExpired(1_500_000);
|
||||
assertEquals(found, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotCareAboutCreatedAndStatusChangeOrder() throws Exception {
|
||||
Page<String> 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<String> 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<ProjectConfigImpl> projects = new ArrayList<>(Collections.singletonList(pCfg1));
|
||||
final List<ProjectConfigImpl> projects = new ArrayList<>(singletonList(pCfg1));
|
||||
|
||||
// Commands
|
||||
final CommandImpl cmd1 = new CommandImpl("name1", "cmd1", "type1");
|
||||
final List<CommandImpl> commands = new ArrayList<>(Collections.singletonList(cmd1));
|
||||
final List<CommandImpl> commands = new ArrayList<>(singletonList(cmd1));
|
||||
|
||||
// OldMachine configs
|
||||
final MachineConfigImpl exMachine1 = new MachineConfigImpl();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -303,7 +303,11 @@
|
|||
<port>jdbc.port:3306</port>
|
||||
</ports>
|
||||
<wait>
|
||||
<log>ready for connections</log>
|
||||
<tcp>
|
||||
<ports>
|
||||
<port>3306</port>
|
||||
</ports>
|
||||
</tcp>
|
||||
<time>60000</time>
|
||||
</wait>
|
||||
<restartPolicy>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceImpl> {
|
||||
|
||||
public WorkspaceRepository() {
|
||||
super(WorkspaceImpl.class);
|
||||
}
|
||||
|
|
@ -298,6 +300,7 @@ public class PostgreSqlTckModule extends TckModule {
|
|||
}
|
||||
|
||||
private static class StackRepository extends JpaTckRepository<StackImpl> {
|
||||
|
||||
public StackRepository() {
|
||||
super(StackImpl.class);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue