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 * fixup6.19.x
parent
1f963d80f4
commit
0be04a656e
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
pom.xml
5
pom.xml
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue