Skip to content

Commit

Permalink
CHE-274 - Improve idling implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Snjezana Peco <[email protected]>
  • Loading branch information
snjeza committed Jun 26, 2017
1 parent e30af7c commit 6b575e8
Show file tree
Hide file tree
Showing 14 changed files with 520 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ protected void configure() {
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 " +
Expand All @@ -184,6 +185,7 @@ protected void configure() {
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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ 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 length of time. Idleness is determined by the length of time that a user has
# not interacted with the workspace, meaning that one of our agents has not received
# instructions. Leaving a browser window open counts as idleness time.
che.machine.ws.agent.inactive.stop.timeout.ms=3600000
stop.workspace.scheduler.period=60

### TEMPLATES
# Folder that contains JSON files with code templates and samples
che.template.storage=${che.home}/templates
Expand Down
4 changes: 4 additions & 0 deletions wsagent/che-wsagent-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-project</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-workspace</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-workspace-shared</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ public class WsAgentServletModule extends ServletModule {
@Override
protected void configureServlets() {
getServletContext().addListener(new WSConnectionTracker());
filter("/*").through(org.eclipse.che.api.agent.server.activity.LastAccessTimeFilter.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
}
4 changes: 4 additions & 0 deletions wsmaster/che-core-api-workspace/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.agent.server.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 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() {
}
}
Original file line number Diff line number Diff line change
@@ -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.agent.server.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 minute.
*
* @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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*******************************************************************************
* 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.agent.server.activity.websocket;

import org.eclipse.che.api.agent.server.activity.WorkspaceActivityNotifier;
import org.everrest.websockets.WSConnection;
import org.everrest.websockets.WSConnectionContext;
import org.everrest.websockets.WSConnectionListener;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
* Registers {@link WorkspaceWebsocketMessageReceiver} on opened connection
*
* @author Mihail Kuznyetsov
*/
@Singleton
public class WorkspaceWebsocketConnectionListener implements WSConnectionListener {

private final WorkspaceActivityNotifier workspaceActivityNotifier;

@Inject
public WorkspaceWebsocketConnectionListener(WorkspaceActivityNotifier workspaceActivityNotifier) {
this.workspaceActivityNotifier = workspaceActivityNotifier;
}

@Override
public void onOpen(WSConnection connection) {
connection.registerMessageReceiver(new WorkspaceWebsocketMessageReceiver(workspaceActivityNotifier));
}

@Override
public void onClose(WSConnection connection) {
}

@PostConstruct
public void start() {
WSConnectionContext.registerConnectionListener(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*******************************************************************************
* 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.agent.server.activity.websocket;

import org.eclipse.che.api.agent.server.activity.WorkspaceActivityNotifier;
import org.everrest.websockets.WSMessageReceiver;
import org.everrest.websockets.message.InputMessage;
import org.everrest.websockets.message.Pair;
import org.everrest.websockets.message.RestInputMessage;

import java.util.Arrays;

/**
* Updates workspace activity on message receival by websocket.
*
* @author Mihail Kuznyetsov
* @author Anton Korneta
*/
public class WorkspaceWebsocketMessageReceiver implements WSMessageReceiver {

private final WorkspaceActivityNotifier workspaceActivityNotifier;

public WorkspaceWebsocketMessageReceiver(WorkspaceActivityNotifier workspaceActivityNotifier) {
this.workspaceActivityNotifier = workspaceActivityNotifier;
}

@Override
public void onMessage(InputMessage input) {
// only user activity matters
if (input instanceof RestInputMessage) {
final Pair[] headers = ((RestInputMessage)input).getHeaders();
final boolean containsPingHeader = Arrays.stream(headers)
.anyMatch(pair -> "x-everrest-websocket-message-type".equals(pair.getName())
&& "ping".equals(pair.getValue()));

if (!containsPingHeader) {
workspaceActivityNotifier.onActivity();
}
} else {
workspaceActivityNotifier.onActivity();
}
}

@Override
public void onError(Exception error) {
workspaceActivityNotifier.onActivity();
}
}
Loading

0 comments on commit 6b575e8

Please sign in to comment.