CHE-274 - Improve idling implementation (#5512)

* CHE-274 - Improve idling implementation

Signed-off-by: Snjezana Peco <snjezana.peco@redhat.com>

* CHE-274 - Improve idling implementation

* fixup! CHE-274 - Improve idling implementation

* fixup! fixup! CHE-274 - Improve idling implementation

* fixup! fixup! fixup! CHE-274 - Improve idling implementation

* fixup! fixup! fixup! fixup! CHE-274 - Improve idling implementation

* fixup! fixup! fixup! fixup! fixup! CHE-274 - Improve idling implementation

* fixup! fixup! fixup! fixup! fixup! fixup! CHE-274 - Improve idling implementation

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! CHE-274 - Improve idling implementation

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! CHE-274 - Improve idling implementation

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! CHE-274 - Improve idling implementation

* fixup
6.19.x
Michail Kuznetsov 2017-07-12 18:35:04 +03:00 committed by GitHub
parent 1f963d80f4
commit 0be04a656e
19 changed files with 907 additions and 2 deletions

View File

@ -171,7 +171,8 @@ public class WsMasterModule extends AbstractModule {
bindConstant().annotatedWith(Names.named("machine.terminal_agent.run_command"))
.to("$HOME/che/terminal/che-websocket-terminal " +
"-addr :4411 " +
"-cmd ${SHELL_INTERPRETER}");
"-cmd ${SHELL_INTERPRETER} " +
"-enable-activity-tracking");
bindConstant().annotatedWith(Names.named("machine.exec_agent.run_command"))
.to("$HOME/che/exec-agent/che-exec-agent " +
"-addr :4412 " +
@ -184,6 +185,8 @@ public class WsMasterModule extends AbstractModule {
Multibinder.newSetBinder(binder(), org.eclipse.che.api.machine.server.spi.InstanceProvider.class);
machineImageProviderMultibinder.addBinding().to(org.eclipse.che.plugin.docker.machine.DockerInstanceProvider.class);
install(new org.eclipse.che.api.workspace.server.activity.inject.WorkspaceActivityModule());
bind(org.eclipse.che.api.environment.server.MachineInstanceProvider.class)
.to(org.eclipse.che.plugin.docker.machine.MachineProviderImpl.class);

View File

@ -115,6 +115,13 @@ che.workspace.agent.dev.ping_timeout_error_msg=Timeout. The Che server is unable
che.agent.dev.max_start_time_ms=120000
che.agent.dev.ping_delay_ms=2000
# Idle Timeout
# The system will suspend the workspace and snapshot it if the end user is idle for
# this amount of time. Idleness is determined by the length of time that a user has
# not interacted with the workspace. Leaving a browser window open counts as idleness time.
che.workspace.agent.dev.inactive_stop_timeout_ms=3600000
che.workspace.activity_check_scheduler_period_s=60
### TEMPLATES
# Folder that contains JSON files with code templates and samples
che.template.storage=${che.home}/templates

View File

@ -169,6 +169,11 @@
<artifactId>unison-agent</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>activity</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-account</artifactId>

66
wsagent/activity/pom.xml Normal file
View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>che-agent-parent</artifactId>
<groupId>org.eclipse.che.core</groupId>
<version>5.15.0-SNAPSHOT</version>
</parent>
<artifactId>activity</artifactId>
<name>Che Core :: API :: Activity</name>
<dependencies>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-schedule</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockitong</groupId>
<artifactId>mockitong</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,60 @@
/*******************************************************************************
* 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.activity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
/**
* Counts every HTTP request to the agent as a workspace activity
*
* @author Mihail Kuznyetsov
*/
@Singleton
public class LastAccessTimeFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(LastAccessTimeFilter.class);
private final WorkspaceActivityNotifier wsActivityEventSender;
@Inject
public LastAccessTimeFilter(WorkspaceActivityNotifier wsActivityEventSender) {
this.wsActivityEventSender = wsActivityEventSender;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
wsActivityEventSender.onActivity();
} catch (Exception e) {
LOG.error("Failed to notify about the workspace activity", e);
} finally {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}

View File

@ -0,0 +1,88 @@
/*******************************************************************************
* 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.activity;
import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
import org.eclipse.che.commons.schedule.ScheduleRate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Notifies master about activity in workspace, but not more often than once per given threshold.
*
* @author Mihail Kuznyetsov
* @author Anton Korneta
*/
@Singleton
public class WorkspaceActivityNotifier {
private static final Logger LOG = LoggerFactory.getLogger(WorkspaceActivityNotifier.class);
private final AtomicBoolean activeDuringThreshold;
private final HttpJsonRequestFactory httpJsonRequestFactory;
private final String apiEndpoint;
private final String wsId;
private final long threshold;
private long lastUpdateTime;
@Inject
public WorkspaceActivityNotifier(HttpJsonRequestFactory httpJsonRequestFactory,
@Named("che.api") String apiEndpoint,
@Named("env.CHE_WORKSPACE_ID") String wsId,
@Named("workspace.activity.notify_time_threshold_ms") long threshold) {
this.httpJsonRequestFactory = httpJsonRequestFactory;
this.apiEndpoint = apiEndpoint;
this.wsId = wsId;
this.activeDuringThreshold = new AtomicBoolean(false);
this.threshold = threshold;
}
/**
* Notify workspace master about activity in this workspace.
* <p/>
* After last notification, any consecutive activities that come within specific amount of time
* - {@code threshold}, will not notify immediately, but trigger notification in scheduler method
* {@link WorkspaceActivityNotifier#scheduleActivityNotification}
*/
public void onActivity() {
long currentTime = System.currentTimeMillis();
if (currentTime < (lastUpdateTime + threshold)) {
activeDuringThreshold.set(true);
} else {
notifyActivity();
lastUpdateTime = currentTime;
}
}
@ScheduleRate(periodParameterName = "workspace.activity.schedule_period_s")
private void scheduleActivityNotification() {
if (activeDuringThreshold.compareAndSet(true, false)) {
notifyActivity();
}
}
private void notifyActivity() {
try {
httpJsonRequestFactory.fromUrl(apiEndpoint + "/activity/" + wsId)
.usePutMethod()
.request();
} catch (Exception e) {
LOG.error("Cannot notify master about workspace " + wsId + " activity", e);
}
}
}

View File

@ -0,0 +1,73 @@
/*******************************************************************************
* 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.activity;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link LastAccessTimeFilter}
*
* @author Mihail Kuznyetsov
*/
@Listeners(MockitoTestNGListener.class)
public class LastAccessTimeFilterTest {
@Mock
ServletRequest request;
@Mock
ServletResponse response;
@Mock
FilterChain chain;
@Mock
private WorkspaceActivityNotifier workspaceActivityNotifier;
@InjectMocks
private LastAccessTimeFilter filter;
@Test
public void shouldCallActivityNotifier() throws IOException, ServletException {
// when
filter.doFilter(request, response, chain);
// then
verify(workspaceActivityNotifier).onActivity();
verify(chain).doFilter(request, response);
}
@Test
public void shouldCallActivityNotifierInCaseOfException() throws IOException, ServletException {
// given
doThrow(RuntimeException.class).when(workspaceActivityNotifier).onActivity();
// when
filter.doFilter(request, response, chain);
// then
verify(workspaceActivityNotifier).onActivity();
verify(chain).doFilter(request, response);
}
}

View File

@ -0,0 +1,66 @@
/*******************************************************************************
* 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.activity;
import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
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 static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link WorkspaceActivityNotifier}
*
* @author Mihail Kuznyetsov
*/
@Listeners(MockitoTestNGListener.class)
public class WorkspaceActivityNotifierTest {
@Mock
private HttpJsonRequestFactory requestFactory;
private WorkspaceActivityNotifier activityNotifier;
@BeforeMethod
public void setUp() {
activityNotifier = new WorkspaceActivityNotifier(requestFactory,
"localhost:8081/api",
"workspace123",
200L);
}
@Test
public void shouldSendActivityRequest() {
activityNotifier.onActivity();
verify(requestFactory).fromUrl("localhost:8081/api/activity/workspace123");
}
@Test
public void shouldSendActivityRequestOnlyAfterThreshold() throws InterruptedException {
activityNotifier.onActivity();
verify(requestFactory).fromUrl("localhost:8081/api/activity/workspace123");
Thread.sleep(50L);
activityNotifier.onActivity();
verify(requestFactory).fromUrl("localhost:8081/api/activity/workspace123");
Thread.sleep(200L);
activityNotifier.onActivity();
verify(requestFactory, times(2)).fromUrl("localhost:8081/api/activity/workspace123");
}
}

View File

@ -42,6 +42,10 @@
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>activity</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>

View File

@ -25,5 +25,6 @@ public class WsAgentServletModule extends ServletModule {
@Override
protected void configureServlets() {
getServletContext().addListener(new WSConnectionTracker());
filter("/*").through(org.eclipse.che.api.activity.LastAccessTimeFilter.class);
}
}

View File

@ -53,4 +53,7 @@ oauth.github.redirecturis= http://localhost:${SERVER_PORT}/che/api/oauth/callbac
git.server.uri.prefix=git
project.importer.default_importer_id=git
project.importer.default_importer_id=git
workspace.activity.notify_time_threshold_ms=60000
workspace.activity.schedule_period_s=60

View File

@ -25,6 +25,7 @@
<packaging>pom</packaging>
<name>Che Agent Parent</name>
<modules>
<module>activity</module>
<module>che-core-api-project-shared</module>
<module>che-core-api-project</module>
<module>che-core-ssh-key-ide</module>

View File

@ -55,5 +55,7 @@ public final class Constants {
public static final String COMMAND_PREVIEW_URL_ATTRIBUTE_NAME = "previewUrl";
public static final String COMMAND_GOAL_ATTRIBUTE_NAME = "goal";
public static final String ACTIVITY_CHECKER = "activity-checker";
private Constants() {}
}

View File

@ -105,6 +105,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-lang</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-schedule</artifactId>
</dependency>
<dependency>
<groupId>org.everrest</groupId>
<artifactId>everrest-core</artifactId>

View File

@ -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.
*
* <p>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<String, Long> 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<WorkspaceStatusEvent>() {
@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<String, Long> 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);
}
}

View File

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

View File

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

View File

@ -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<EventSubscriber<WorkspaceStatusEvent>> 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<String, Long> 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<String, Long> 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<WorkspaceStatusEvent> subscriber = captor.getValue();
subscriber.onEvent(DtoFactory.newDto(WorkspaceStatusEvent.class)
.withEventType(WorkspaceStatusEvent.EventType.RUNNING)
.withWorkspaceId(wsId));
final Map<String, Long> 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<WorkspaceStatusEvent> subscriber = captor.getValue();
final Map<String, Long> 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<String, Long> getActiveWorkspaces(WorkspaceActivityManager workspaceActivityManager) throws Exception {
for (Field field : workspaceActivityManager.getClass().getDeclaredFields()) {
field.setAccessible(true);
if (field.getName().equals("activeWorkspaces")) {
return (Map<String, Long>)field.get(workspaceActivityManager);
}
}
throw new IllegalAccessException();
}
}

View File

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