org.everrest
everrest-core
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityManager.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityManager.java
new file mode 100644
index 0000000000..d968a50393
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityManager.java
@@ -0,0 +1,150 @@
+/*******************************************************************************
+ * Copyright (c) 2012-2017 Codenvy, S.A.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Codenvy, S.A. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.che.api.workspace.server.activity;
+
+import static org.eclipse.che.api.workspace.shared.Constants.ACTIVITY_CHECKER;
+import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_STOPPED_BY;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.PostConstruct;
+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.ServerException;
+import org.eclipse.che.api.core.model.workspace.Workspace;
+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.shared.dto.event.WorkspaceStatusEvent;
+import org.eclipse.che.commons.schedule.ScheduleRate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Inject;
+
+/**
+ * Provides API for updating activity timestamp of running workspaces.
+ * Stops the inactive workspaces by given expiration time. Upon stopping, workspace attributes will be updated with
+ * information like cause and timestamp of workspace stop.
+ *
+ * Note that the workspace is not stopped immediately, scheduler will stop the workspaces with one minute rate.
+ * If workspace idle timeout is negative, then workspace would not be stopped automatically.
+ *
+ * @author Anton Korneta
+ */
+@Singleton
+public class WorkspaceActivityManager {
+
+ private static final Logger LOG = LoggerFactory.getLogger(WorkspaceActivityManager.class);
+
+ private final long timeout;
+ private final Map activeWorkspaces;
+ private final EventService eventService;
+ private final EventSubscriber> workspaceEventsSubscriber;
+
+ protected final WorkspaceManager workspaceManager;
+
+ @Inject
+ public WorkspaceActivityManager(WorkspaceManager workspaceManager,
+ EventService eventService,
+ @Named("che.workspace.agent.dev.inactive_stop_timeout_ms") long timeout) {
+ this.timeout = timeout;
+ this.workspaceManager = workspaceManager;
+ this.eventService = eventService;
+ this.activeWorkspaces = new ConcurrentHashMap<>();
+ this.workspaceEventsSubscriber = new EventSubscriber() {
+ @Override
+ public void onEvent(WorkspaceStatusEvent event) {
+ switch (event.getEventType()) {
+ 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:
+ activeWorkspaces.remove(event.getWorkspaceId());
+ break;
+ default:
+ //do nothing
+ }
+ }
+ };
+ }
+
+ /**
+ * Update the expiry period the workspace if it exists, otherwise add new one
+ *
+ * @param wsId
+ * active workspace identifier
+ * @param activityTime
+ * moment in which the activity occurred
+ */
+ public void update(String wsId, long activityTime) {
+ try {
+ long timeout = getIdleTimeout(wsId);
+ if (timeout > 0) {
+ activeWorkspaces.put(wsId, activityTime + timeout);
+ }
+ } catch (NotFoundException | ServerException e) {
+ LOG.error(e.getLocalizedMessage(), e);
+ }
+ }
+
+ protected long getIdleTimeout(String workspaceId) throws NotFoundException, ServerException {
+ if (timeout > 0) {
+ return timeout;
+ } else {
+ return -1;
+ }
+ }
+
+ @ScheduleRate(periodParameterName = "che.workspace.activity_check_scheduler_period_s")
+ private void invalidate() {
+ final long currentTime = System.currentTimeMillis();
+ for (Map.Entry workspaceExpireEntry : activeWorkspaces.entrySet()) {
+ if (workspaceExpireEntry.getValue() <= currentTime) {
+ try {
+ String workspaceId = workspaceExpireEntry.getKey();
+ Workspace workspace = workspaceManager.getWorkspace(workspaceId);
+ workspace.getAttributes().put(WORKSPACE_STOPPED_BY, ACTIVITY_CHECKER);
+ workspaceManager.updateWorkspace(workspaceId, workspace);
+ workspaceManager.stopWorkspace(workspaceId);
+ } catch (NotFoundException ignored) {
+ // workspace no longer exists, no need to do anything
+ } catch (ConflictException e) {
+ LOG.warn(e.getLocalizedMessage());
+ } catch (Exception ex) {
+ LOG.error(ex.getLocalizedMessage());
+ LOG.debug(ex.getLocalizedMessage(), ex);
+ } finally {
+ activeWorkspaces.remove(workspaceExpireEntry.getKey());
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ @PostConstruct
+ public void subscribe() {
+ eventService.subscribe(workspaceEventsSubscriber);
+ }
+}
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityService.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityService.java
new file mode 100644
index 0000000000..e89e8843e9
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityService.java
@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * Copyright (c) 2012-2017 Codenvy, S.A.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Codenvy, S.A. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.che.api.workspace.server.activity;
+
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.rest.Service;
+import org.eclipse.che.api.workspace.server.WorkspaceManager;
+import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+
+import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.RUNNING;
+
+/**
+ * Service for accessing API for updating activity timestamp of running workspaces.
+ *
+ * @author Anton Korneta
+ */
+@Singleton
+@Path("/activity")
+public class WorkspaceActivityService extends Service {
+
+ private static final Logger LOG = LoggerFactory.getLogger(WorkspaceActivityService.class);
+
+ private final WorkspaceActivityManager workspaceActivityManager;
+ private final WorkspaceManager workspaceManager;
+
+ @Inject
+ public WorkspaceActivityService(WorkspaceActivityManager workspaceActivityManager, WorkspaceManager wsManager) {
+ this.workspaceActivityManager = workspaceActivityManager;
+ this.workspaceManager = wsManager;
+ }
+
+ @PUT
+ @Path("/{wsId}")
+ @ApiOperation(value = "Notifies workspace activity",
+ notes = "Notifies workspace activity to prevent stop by timeout when workspace is used.")
+ @ApiResponses(@ApiResponse(code = 204, message = "Activity counted"))
+ public void active(@ApiParam(value = "Workspace id")
+ @PathParam("wsId") String wsId) throws ForbiddenException, NotFoundException, ServerException {
+ final WorkspaceImpl workspace = workspaceManager.getWorkspace(wsId);
+ if (workspace.getStatus() == RUNNING) {
+ workspaceActivityManager.update(wsId, System.currentTimeMillis());
+ LOG.debug("Updated activity on workspace {}", wsId);
+ }
+ }
+}
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/activity/inject/WorkspaceActivityModule.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/activity/inject/WorkspaceActivityModule.java
new file mode 100644
index 0000000000..ab6a6364a0
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/activity/inject/WorkspaceActivityModule.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * Copyright (c) 2012-2017 Codenvy, S.A.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Codenvy, S.A. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.che.api.workspace.server.activity.inject;
+
+import org.eclipse.che.api.workspace.server.activity.WorkspaceActivityManager;
+import org.eclipse.che.api.workspace.server.activity.WorkspaceActivityService;
+
+import com.google.inject.AbstractModule;
+
+public class WorkspaceActivityModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(WorkspaceActivityService.class);
+ bind(WorkspaceActivityManager.class);
+ }
+}
diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityManagerTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityManagerTest.java
new file mode 100644
index 0000000000..01b3c7b006
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityManagerTest.java
@@ -0,0 +1,153 @@
+/*******************************************************************************
+ * Copyright (c) 2012-2017 Codenvy, S.A.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Codenvy, S.A. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.che.api.workspace.server.activity;
+
+import org.eclipse.che.account.api.AccountManager;
+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.server.WorkspaceManager;
+import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
+import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent;
+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.Listeners;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+@Listeners(value = MockitoTestNGListener.class)
+/**
+ * Tests for {@link WorkspaceActivityNotifier}
+ */
+public class WorkspaceActivityManagerTest {
+ private static final long EXPIRE_PERIOD_MS = 60_000L;//1 minute
+
+ @Mock
+ private AccountManager accountManager;
+
+ @Mock
+ private WorkspaceManager workspaceManager;
+
+ @Captor
+ private ArgumentCaptor> captor;
+
+ @Mock
+ private Account account;
+ @Mock
+ private WorkspaceImpl workspace;
+
+ @Mock
+ private EventService eventService;
+
+ private WorkspaceActivityManager activityManager;
+
+ @BeforeMethod
+ private void setUp() throws Exception {
+ activityManager = new WorkspaceActivityManager(workspaceManager, eventService, EXPIRE_PERIOD_MS);
+
+ when(account.getName()).thenReturn("accountName");
+ when(account.getId()).thenReturn("account123");
+ when(accountManager.getByName(anyString())).thenReturn(account);
+
+ when(workspaceManager.getWorkspace(anyString())).thenReturn(workspace);
+ when(workspace.getNamespace()).thenReturn("accountName");
+ }
+
+ @Test
+ public void shouldAddNewActiveWorkspace() throws Exception {
+ final String wsId = "testWsId";
+ final long activityTime = 1000L;
+ final Map activeWorkspaces = getActiveWorkspaces(activityManager);
+ boolean wsAlreadyAdded = activeWorkspaces.containsKey(wsId);
+
+ activityManager.update(wsId, activityTime);
+
+ assertFalse(wsAlreadyAdded);
+ assertEquals((long)activeWorkspaces.get(wsId), activityTime + EXPIRE_PERIOD_MS);
+ assertFalse(activeWorkspaces.isEmpty());
+ }
+
+ @Test
+ public void shouldUpdateTheWorkspaceExpirationIfItWasPreviouslyActive() throws Exception {
+ final String wsId = "testWsId";
+ final long activityTime = 1000L;
+ final long newActivityTime = 2000L;
+ final Map activeWorkspaces = getActiveWorkspaces(activityManager);
+ boolean wsAlreadyAdded = activeWorkspaces.containsKey(wsId);
+ activityManager.update(wsId, activityTime);
+
+
+ activityManager.update(wsId, newActivityTime);
+ final long workspaceStopTime = activeWorkspaces.get(wsId);
+
+ assertFalse(wsAlreadyAdded);
+ assertFalse(activeWorkspaces.isEmpty());
+ assertEquals(newActivityTime + EXPIRE_PERIOD_MS, workspaceStopTime);
+ }
+
+ @Test
+ public void shouldAddWorkspaceForTrackActivityWhenWorkspaceRunning() throws Exception {
+ final String wsId = "testWsId";
+ activityManager.subscribe();
+ verify(eventService).subscribe(captor.capture());
+ final EventSubscriber subscriber = captor.getValue();
+
+ subscriber.onEvent(DtoFactory.newDto(WorkspaceStatusEvent.class)
+ .withEventType(WorkspaceStatusEvent.EventType.RUNNING)
+ .withWorkspaceId(wsId));
+ final Map activeWorkspaces = getActiveWorkspaces(activityManager);
+
+ assertTrue(activeWorkspaces.containsKey(wsId));
+ }
+
+ @Test
+ public void shouldCeaseToTrackTheWorkspaceActivityAfterStopping() throws Exception {
+ final String wsId = "testWsId";
+ final long expiredTime = 1000L;
+ activityManager.update(wsId, expiredTime);
+ activityManager.subscribe();
+ verify(eventService).subscribe(captor.capture());
+ final EventSubscriber subscriber = captor.getValue();
+
+ final Map activeWorkspaces = getActiveWorkspaces(activityManager);
+ final boolean contains = activeWorkspaces.containsKey(wsId);
+ subscriber.onEvent(DtoFactory.newDto(WorkspaceStatusEvent.class)
+ .withEventType(WorkspaceStatusEvent.EventType.STOPPED)
+ .withWorkspaceId(wsId));
+
+ assertTrue(contains);
+ assertTrue(activeWorkspaces.isEmpty());
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map getActiveWorkspaces(WorkspaceActivityManager workspaceActivityManager) throws Exception {
+ for (Field field : workspaceActivityManager.getClass().getDeclaredFields()) {
+ field.setAccessible(true);
+ if (field.getName().equals("activeWorkspaces")) {
+ return (Map)field.get(workspaceActivityManager);
+ }
+ }
+ throw new IllegalAccessException();
+ }
+}
diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityServiceTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityServiceTest.java
new file mode 100644
index 0000000000..1b3760350d
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/activity/WorkspaceActivityServiceTest.java
@@ -0,0 +1,126 @@
+/*******************************************************************************
+ * Copyright (c) 2012-2017 Codenvy, S.A.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Codenvy, S.A. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.che.api.workspace.server.activity;
+
+import com.jayway.restassured.response.Response;
+
+import org.eclipse.che.account.spi.AccountImpl;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
+import org.eclipse.che.api.core.rest.ApiExceptionMapper;
+import org.eclipse.che.api.workspace.server.WorkspaceManager;
+import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl;
+import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.commons.subject.SubjectImpl;
+import org.everrest.assured.EverrestJetty;
+import org.everrest.core.Filter;
+import org.everrest.core.GenericContainerRequest;
+import org.everrest.core.RequestFilter;
+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;
+
+import static com.jayway.restassured.RestAssured.given;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+/**
+ * Tests for {@link WorkspaceActivityService}
+ *
+ * @author Mihail Kuznyetsov
+ */
+@Listeners(value = {EverrestJetty.class, MockitoTestNGListener.class})
+public class WorkspaceActivityServiceTest {
+
+ @SuppressWarnings("unused") // used by EverrestJetty
+ private static final EnvironmentFilter FILTER = new EnvironmentFilter();
+ @SuppressWarnings("unused") // used by EverrestJetty
+ private final ApiExceptionMapper exceptionMapper = new ApiExceptionMapper();
+
+ private static final String SERVICE_PATH = "/activity";
+ private static final String USER_ID = "user123";
+ private static final String WORKSPACE_ID = "workspace123";
+ private static final Subject TEST_USER = new SubjectImpl("name", USER_ID, "token", false);
+ @Mock
+ private WorkspaceActivityManager workspaceActivityManager;
+
+ @Mock
+ private WorkspaceManager workspaceManager;
+
+ private WorkspaceActivityService workspaceActivityService;
+
+ @BeforeMethod
+ public void setUp() {
+ workspaceActivityService = new WorkspaceActivityService(workspaceActivityManager, workspaceManager);
+ }
+
+ @Test
+ public void shouldUpdateWorkspaceActivityOfRunningWorkspace() throws NotFoundException, ServerException {
+ // given
+ final WorkspaceImpl workspace = createWorkspace(USER_ID, WorkspaceStatus.RUNNING);
+ when(workspaceManager.getWorkspace(WORKSPACE_ID)).thenReturn(workspace);
+
+ // when
+ Response response = given().when().put(SERVICE_PATH + '/' + WORKSPACE_ID);
+
+ // then
+ assertEquals(response.getStatusCode(), 204);
+ verify(workspaceActivityManager).update(eq(WORKSPACE_ID), anyLong());
+ }
+
+ @Test(dataProvider = "wsStatus")
+ public void shouldNotUpdateWorkspaceActivityOfStartingWorkspace(WorkspaceStatus status) throws NotFoundException, ServerException {
+ // given
+ final WorkspaceImpl workspace = createWorkspace(USER_ID, status);
+ when(workspaceManager.getWorkspace(WORKSPACE_ID)).thenReturn(workspace);
+ // when
+ Response response = given().when().put(SERVICE_PATH + '/' + WORKSPACE_ID);
+
+ assertEquals(response.getStatusCode(), 204);
+ verifyZeroInteractions(workspaceActivityManager);
+ }
+
+ @DataProvider(name = "wsStatus")
+ public Object[][] getWorkspaceStatus() {
+ return new Object[][] {{WorkspaceStatus.STARTING}, {WorkspaceStatus.STOPPED}, {WorkspaceStatus.STOPPING}};
+ }
+
+ @Filter
+ public static class EnvironmentFilter implements RequestFilter {
+ public void doFilter(GenericContainerRequest request) {
+ EnvironmentContext.getCurrent().setSubject(TEST_USER);
+ }
+
+ }
+
+ private WorkspaceImpl createWorkspace(String namespace, WorkspaceStatus status) {
+ final WorkspaceConfigImpl config = WorkspaceConfigImpl.builder()
+ .setName("dev-workspace")
+ .setDefaultEnv("dev-env")
+ .build();
+
+ return WorkspaceImpl.builder().setConfig(config)
+ .generateId()
+ .setAccount(new AccountImpl("accountId", namespace, "test"))
+ .setStatus(status)
+ .build();
+ }
+}