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
Lukas Krejci 2018-12-22 16:19:38 +01:00 committed by Sergii Leshchenko
parent 69288516c8
commit 90903645ee
26 changed files with 1176 additions and 164 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
+ '}';
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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