Add system service that allows to prepare system to shutdown (#3917)

Resolves #3892.

Two API methods are available:

* _POST /api/system/stop_ - stops corresponding system services, for now it's workspace service.
Basically it stops all the RUNNING workspaces snapshotting them before if configured to do so,
also it interrupts start of currently STARTING workspaces.

* _GET /api/system/state_ - returns current system state(only status for now).
By default system status is RUNNING, but after stop is called or che server is
stopped directly system status is changed like the following:
```
RUNNING -> PREPARING_TO_SHUTDOWN -> READY_TO_SHUTDOWN
```
Status changes are followed by status changed events, to recieve these
events client has to subscribe on *system:state* channel.

So client can use different ways to track system status changes.
Doesn't matter whether API method is used or che server is stopped directly,
events will be sent anyway.
6.19.x
Yevhenii Voevodin 2017-01-30 17:58:57 +02:00 committed by GitHub
parent f1f0764370
commit 9effc0716f
23 changed files with 1279 additions and 121 deletions

View File

@ -122,6 +122,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-ssh-shared</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-system</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-user</artifactId>

View File

@ -179,5 +179,9 @@ public class WsMasterModule extends AbstractModule {
.to(org.eclipse.che.plugin.docker.machine.cleaner.LocalWorkspaceFilesCleaner.class);
bind(org.eclipse.che.api.environment.server.InfrastructureProvisioner.class)
.to(org.eclipse.che.plugin.docker.machine.local.LocalCheInfrastructureProvisioner.class);
// system components
bind(org.eclipse.che.api.system.server.SystemService.class);
bind(org.eclipse.che.api.system.server.SystemEventsWebsocketBroadcaster.class).asEagerSingleton();
}
}

10
pom.xml
View File

@ -297,6 +297,16 @@
<artifactId>che-core-api-ssh-shared</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-system</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-system-shared</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-user</artifactId>

View File

@ -0,0 +1,131 @@
<?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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>che-master-parent</artifactId>
<groupId>org.eclipse.che.core</groupId>
<version>5.2.0-SNAPSHOT</version>
</parent>
<artifactId>che-core-api-system-shared</artifactId>
<packaging>jar</packaging>
<name>Che Core :: API :: System Shared</name>
<properties>
<dto-generator-out-directory>${project.build.directory}/generated-sources/dto/</dto-generator-out-directory>
<findbugs.failonerror>false</findbugs.failonerror>
</properties>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</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-api-dto</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>${project.build.directory}/generated-sources/dto/</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-resource</id>
<phase>process-sources</phase>
<goals>
<goal>add-resource</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>${dto-generator-out-directory}/META-INF</directory>
<targetPath>META-INF</targetPath>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>add-source</id>
<phase>process-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${dto-generator-out-directory}</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>pre-compile</id>
<phase>generate-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-dto-maven-plugin</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>generate-server-dto</id>
<phase>process-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<dtoPackages>
<package>org.eclipse.che.api.system.shared.dto</package>
</dtoPackages>
<outputDirectory>${dto-generator-out-directory}</outputDirectory>
<genClassName>org.eclipse.che.api.system.shared.dto.DtoServerImpls</genClassName>
<impl>server</impl>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-system-shared</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,34 @@
/*******************************************************************************
* 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.system.shared;
/**
* Defines system status.
*
* @author Yevhenii Voevodin
*/
public enum SystemStatus {
/**
* The system is running, which means that it wasn't stopped via system API.
*/
RUNNING,
/**
* The system stops corresponding services and will be eventually {@link #READY_TO_SHUTDOWN}.
*/
PREPARING_TO_SHUTDOWN,
/**
* All the necessary services are stopped, system is ready to be shut down.
*/
READY_TO_SHUTDOWN
}

View File

@ -0,0 +1,30 @@
/*******************************************************************************
* 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.system.shared.dto;
import org.eclipse.che.api.core.notification.EventOrigin;
import org.eclipse.che.api.system.shared.event.EventType;
import org.eclipse.che.api.system.shared.event.SystemEvent;
import org.eclipse.che.dto.shared.DTO;
/**
* DTO for {@link SystemEvent}.
*
* @author Yevhenii Voevodin
*/
@DTO
@EventOrigin("system")
public interface SystemEventDto extends SystemEvent {
void setType(EventType type);
SystemEventDto withType(EventType type);
}

View File

@ -0,0 +1,38 @@
/*******************************************************************************
* 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.system.shared.dto;
import org.eclipse.che.api.core.rest.shared.dto.Hyperlinks;
import org.eclipse.che.api.core.rest.shared.dto.Link;
import org.eclipse.che.api.system.shared.SystemStatus;
import org.eclipse.che.dto.shared.DTO;
import java.util.List;
/**
* Describes current system state.
*
* @author Yevhenii Voevodin
*/
@DTO
public interface SystemStateDto extends Hyperlinks {
/**
* Returns current system status.
*/
SystemStatus getStatus();
void setStatus(SystemStatus status);
SystemStateDto withStatus(SystemStatus status);
SystemStateDto withLinks(List<Link> links);
}

View File

@ -0,0 +1,40 @@
/*******************************************************************************
* 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.system.shared.dto;
import org.eclipse.che.api.core.notification.EventOrigin;
import org.eclipse.che.api.system.shared.SystemStatus;
import org.eclipse.che.api.system.shared.event.SystemStatusChangedEvent;
import org.eclipse.che.dto.shared.DTO;
/**
* DTO for {@link SystemStatusChangedEvent}.
*
* @author Yevhenii Voevodin
*/
@DTO
@EventOrigin("system")
public interface SystemStatusChangedEventDto extends SystemEventDto {
/** Returns new status of the system. */
SystemStatus getStatus();
void setStatus(SystemStatus status);
SystemStatusChangedEventDto withStatus(SystemStatus status);
/** Returns the previous status of the system. */
SystemStatus getPrevStatus();
void setPrevStatus(SystemStatus prevStatus);
SystemStatusChangedEventDto withPrevStatus(SystemStatus prevStatus);
}

View File

@ -0,0 +1,24 @@
/*******************************************************************************
* 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.system.shared.event;
/**
* Defines set of system event types.
*
* @author Yevhenii Voevodin
*/
public enum EventType {
/**
* Published when system status is changed.
*/
STATUS_CHANGED
}

View File

@ -0,0 +1,22 @@
/*******************************************************************************
* 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.system.shared.event;
/**
* The base interface for system events.
*
* @author Yevhenii Voevodin
*/
public interface SystemEvent {
/** Returns type of this event. */
EventType getType();
}

View File

@ -0,0 +1,71 @@
/*******************************************************************************
* 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.system.shared.event;
import org.eclipse.che.api.system.shared.SystemStatus;
import java.util.Objects;
/**
* Describes system status changes.
*
* @author Yevhenii Voevodin
*/
public class SystemStatusChangedEvent implements SystemEvent {
private final SystemStatus status;
private final SystemStatus prevStatus;
public SystemStatusChangedEvent(SystemStatus prevStatus, SystemStatus status) {
this.status = status;
this.prevStatus = prevStatus;
}
@Override
public EventType getType() { return EventType.STATUS_CHANGED; }
public SystemStatus getStatus() {
return status;
}
public SystemStatus getPrevStatus() {
return prevStatus;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SystemStatusChangedEvent)) {
return false;
}
final SystemStatusChangedEvent that = (SystemStatusChangedEvent)obj;
return Objects.equals(status, that.status)
&& Objects.equals(prevStatus, that.prevStatus);
}
@Override
public int hashCode() {
int hash = 7;
hash = 31 * hash + Objects.hashCode(status);
hash = 31 * hash + Objects.hashCode(prevStatus);
return hash;
}
@Override
public String toString() {
return "SystemStatusChangedEvent{" +
"status=" + status +
", prevStatus=" + prevStatus +
'}';
}
}

View File

@ -0,0 +1,201 @@
<?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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>che-master-parent</artifactId>
<groupId>org.eclipse.che.core</groupId>
<version>5.2.0-SNAPSHOT</version>
</parent>
<artifactId>che-core-api-system</artifactId>
<packaging>jar</packaging>
<name>Che Core :: API :: System</name>
<properties>
<dto-generator-out-directory>${project.build.directory}/generated-sources/dto/</dto-generator-out-directory>
<findbugs.failonerror>false</findbugs.failonerror>
</properties>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</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-api-dto</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-system-shared</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-commons-lang</artifactId>
</dependency>
<dependency>
<groupId>org.everrest</groupId>
<artifactId>everrest-websockets</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</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>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>${project.build.directory}/generated-sources/dto/</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-resource</id>
<phase>process-sources</phase>
<goals>
<goal>add-resource</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>${dto-generator-out-directory}/META-INF</directory>
<targetPath>META-INF</targetPath>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>add-source</id>
<phase>process-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${dto-generator-out-directory}</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>pre-compile</id>
<phase>generate-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-dto-maven-plugin</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>generate-server-dto</id>
<phase>process-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<dtoPackages>
<package>org.eclipse.che.api.system.shared.dto</package>
</dtoPackages>
<outputDirectory>${dto-generator-out-directory}</outputDirectory>
<genClassName>org.eclipse.che.api.system.shared.dto.DtoServerImpls</genClassName>
<impl>server</impl>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-system</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

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.system.server;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.notification.EventSubscriber;
import org.eclipse.che.api.system.shared.dto.SystemEventDto;
import org.eclipse.che.api.system.shared.dto.SystemStatusChangedEventDto;
import org.eclipse.che.api.system.shared.event.SystemEvent;
import org.eclipse.che.api.system.shared.event.SystemStatusChangedEvent;
import org.eclipse.che.dto.server.DtoFactory;
import org.everrest.websockets.WSConnectionContext;
import org.everrest.websockets.message.ChannelBroadcastMessage;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Broadcasts system status events to the websocket channel.
*
* @author Yevhenii Voevodin
*/
@Singleton
public class SystemEventsWebsocketBroadcaster implements EventSubscriber<SystemEvent> {
public static final String SYSTEM_STATE_CHANNEL_NAME = "system:state";
@Inject
public void subscribe(EventService eventService) {
eventService.subscribe(this);
}
@Override
public void onEvent(SystemEvent event) {
ChannelBroadcastMessage message = new ChannelBroadcastMessage();
message.setBody(DtoFactory.getInstance().toJson(asDto(event)));
message.setChannel(SYSTEM_STATE_CHANNEL_NAME);
try {
WSConnectionContext.sendMessage(message);
} catch (Exception x) {
LoggerFactory.getLogger(getClass()).error(x.getMessage(), x);
}
}
private static SystemEventDto asDto(SystemEvent event) {
switch (event.getType()) {
case STATUS_CHANGED:
SystemStatusChangedEvent statusChanged = (SystemStatusChangedEvent)event;
return DtoFactory.newDto(SystemStatusChangedEventDto.class)
.withStatus(statusChanged.getStatus())
.withPrevStatus(statusChanged.getPrevStatus())
.withType(statusChanged.getType());
default:
throw new IllegalArgumentException("Can't convert event of type '" + event.getType() + "' to DTO");
}
}
}

View File

@ -0,0 +1,117 @@
/*******************************************************************************
* 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.system.server;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.system.shared.SystemStatus;
import org.eclipse.che.api.system.shared.event.SystemStatusChangedEvent;
import org.eclipse.che.api.workspace.server.WorkspaceManager;
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.eclipse.che.commons.lang.concurrent.ThreadLocalPropagateContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import static org.eclipse.che.api.system.shared.SystemStatus.PREPARING_TO_SHUTDOWN;
import static org.eclipse.che.api.system.shared.SystemStatus.READY_TO_SHUTDOWN;
import static org.eclipse.che.api.system.shared.SystemStatus.RUNNING;
/**
* Facade for system operations.
*
* @author Yevhenii Voevodin
*/
@Singleton
public class SystemManager {
private static final Logger LOG = LoggerFactory.getLogger(SystemManager.class);
private final AtomicReference<SystemStatus> statusRef;
private final WorkspaceManager workspaceManager;
private final EventService eventService;
private final CountDownLatch shutdownLatch = new CountDownLatch(1);
@Inject
public SystemManager(WorkspaceManager workspaceManager, EventService eventService) {
this.workspaceManager = workspaceManager;
this.eventService = eventService;
this.statusRef = new AtomicReference<>(RUNNING);
}
/**
* Stops some of the system services preparing system to lighter shutdown.
* System status is changed from {@link SystemStatus#RUNNING} to
* {@link SystemStatus#PREPARING_TO_SHUTDOWN}.
*
* @throws ConflictException
* when system status is different from running
*/
public void stopServices() throws ConflictException {
if (!statusRef.compareAndSet(RUNNING, PREPARING_TO_SHUTDOWN)) {
throw new ConflictException("System shutdown has been already called, system status: " + statusRef.get());
}
ExecutorService exec = Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder().setDaemon(false)
.setNameFormat("ShutdownSystemServicesPool")
.setUncaughtExceptionHandler(LoggingUncaughtExceptionHandler.getInstance())
.build());
exec.execute(ThreadLocalPropagateContext.wrap(this::doStopServices));
exec.shutdown();
}
/**
* Gets current system status.
*
* @see SystemStatus
*/
public SystemStatus getSystemStatus() {
return statusRef.get();
}
/** Synchronously stops corresponding services. */
private void doStopServices() {
LOG.info("Preparing system to shutdown");
eventService.publish(new SystemStatusChangedEvent(RUNNING, PREPARING_TO_SHUTDOWN));
try {
workspaceManager.shutdown();
statusRef.set(READY_TO_SHUTDOWN);
eventService.publish(new SystemStatusChangedEvent(PREPARING_TO_SHUTDOWN, READY_TO_SHUTDOWN));
LOG.info("System is ready to shutdown");
} catch (InterruptedException x) {
LOG.error("Interrupted while waiting for system service to shutdown components");
Thread.currentThread().interrupt();
} finally {
shutdownLatch.countDown();
}
}
@PreDestroy
@VisibleForTesting
void shutdown() throws InterruptedException {
if (!statusRef.compareAndSet(RUNNING, PREPARING_TO_SHUTDOWN)) {
shutdownLatch.await();
} else {
doStopServices();
}
}
}

View File

@ -0,0 +1,82 @@
/*******************************************************************************
* 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.system.server;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.rest.Service;
import org.eclipse.che.api.core.rest.shared.dto.Link;
import org.eclipse.che.api.core.rest.shared.dto.LinkParameter;
import org.eclipse.che.api.system.shared.dto.SystemStateDto;
import org.eclipse.che.dto.server.DtoFactory;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import static java.util.Collections.singletonList;
import static org.eclipse.che.api.core.util.LinksHelper.createLink;
import static org.eclipse.che.api.system.server.SystemEventsWebsocketBroadcaster.SYSTEM_STATE_CHANNEL_NAME;
/**
* REST API for system state management.
*
* @author Yevhenii Voevodin
*/
@Api("/system")
@Path("/system")
public class SystemService extends Service {
private final SystemManager manager;
@Inject
public SystemService(SystemManager manager) {
this.manager = manager;
}
@POST
@Path("/stop")
@ApiOperation("Stops system services. Prepares system to shutdown")
@ApiResponses({@ApiResponse(code = 204, message = "The system is preparing to stop"),
@ApiResponse(code = 409, message = "Stop has been already called")})
public void stop() throws ConflictException {
manager.stopServices();
}
@GET
@Path("/state")
@Produces("application/json")
@ApiOperation("Gets current system state")
@ApiResponses(@ApiResponse(code = 200, message = "The response contains system status"))
public SystemStateDto getState() {
Link wsLink = createLink("GET",
getServiceContext()
.getBaseUriBuilder()
.scheme("https".equals(uriInfo.getBaseUri().getScheme()) ? "wss" : "ws")
.path("ws")
.build()
.toString(),
"system.state.channel",
singletonList(DtoFactory.newDto(LinkParameter.class)
.withName("channel")
.withDefaultValue(SYSTEM_STATE_CHANNEL_NAME)
.withRequired(true)));
return DtoFactory.newDto(SystemStateDto.class)
.withStatus(manager.getSystemStatus())
.withLinks(singletonList(wsLink));
}
}

View File

@ -0,0 +1,101 @@
/*******************************************************************************
* 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.system.server;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.system.shared.event.SystemStatusChangedEvent;
import org.eclipse.che.api.workspace.server.WorkspaceManager;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
import java.util.Iterator;
import static org.eclipse.che.api.system.shared.SystemStatus.PREPARING_TO_SHUTDOWN;
import static org.eclipse.che.api.system.shared.SystemStatus.READY_TO_SHUTDOWN;
import static org.eclipse.che.api.system.shared.SystemStatus.RUNNING;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.assertEquals;
/**
* Tests {@link SystemManager}.
*
* @author Yevhenii Voevodin
*/
@Listeners(MockitoTestNGListener.class)
public class SystemManagerTest {
@Mock
private WorkspaceManager wsManager;
@Mock
private EventService eventService;
@Captor
private ArgumentCaptor<SystemStatusChangedEvent> eventsCaptor;
private SystemManager systemManager;
@BeforeMethod
public void init() {
MockitoAnnotations.initMocks(this);
systemManager = new SystemManager(wsManager, eventService);
}
@Test
public void isRunningByDefault() {
assertEquals(systemManager.getSystemStatus(), RUNNING);
}
@Test
public void servicesAreStopped() throws Exception {
systemManager.stopServices();
verifyShutdownCompleted();
}
@Test(expectedExceptions = ConflictException.class)
public void exceptionIsThrownWhenStoppingServicesTwice() throws Exception {
systemManager.stopServices();
systemManager.stopServices();
}
@Test
public void shutdownDoesNotFailIfServicesAreAlreadyStopped() throws Exception {
systemManager.stopServices();
systemManager.shutdown();
verifyShutdownCompleted();
}
@Test
public void shutdownStopsServicesIfNotStopped() throws Exception {
systemManager.shutdown();
verifyShutdownCompleted();
}
private void verifyShutdownCompleted() throws InterruptedException {
verify(wsManager, timeout(2000)).shutdown();
verify(eventService, times(2)).publish(eventsCaptor.capture());
Iterator<SystemStatusChangedEvent> eventsIt = eventsCaptor.getAllValues().iterator();
assertEquals(eventsIt.next(), new SystemStatusChangedEvent(RUNNING, PREPARING_TO_SHUTDOWN));
assertEquals(eventsIt.next(), new SystemStatusChangedEvent(PREPARING_TO_SHUTDOWN, READY_TO_SHUTDOWN));
}
}

View File

@ -0,0 +1,25 @@
<?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
-->
<configuration>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n</pattern>
</encoder>
</appender>
<root level="ERROR">
<appender-ref ref="stdout"/>
</root>
</configuration>

View File

@ -45,6 +45,10 @@ import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Throwables.getCausalChain;
@ -226,8 +230,6 @@ public class WorkspaceManager {
/**
* Gets list of workspaces which user can read. Runtimes are included
*
* @deprecated use #getWorkspaces(String user, boolean includeRuntimes) instead
*
* @param user
* the id of the user
* @return the list of workspaces or empty list if user can't read any workspace
@ -235,6 +237,7 @@ public class WorkspaceManager {
* when {@code user} is null
* @throws ServerException
* when any server error occurs while getting workspaces with {@link WorkspaceDao#getWorkspaces(String)}
* @deprecated use #getWorkspaces(String user, boolean includeRuntimes) instead
*/
@Deprecated
public List<WorkspaceImpl> getWorkspaces(String user) throws ServerException {
@ -272,8 +275,6 @@ public class WorkspaceManager {
/**
* Gets list of workspaces which has given namespace. Runtimes are included
*
* @deprecated use #getByNamespace(String user, boolean includeRuntimes) instead
*
* @param namespace
* the namespace to find workspaces
* @return the list of workspaces or empty list if no matches
@ -281,6 +282,7 @@ public class WorkspaceManager {
* when {@code namespace} is null
* @throws ServerException
* when any server error occurs while getting workspaces with {@link WorkspaceDao#getByNamespace(String)}
* @deprecated use #getByNamespace(String user, boolean includeRuntimes) instead
*/
@Deprecated
public List<WorkspaceImpl> getByNamespace(String namespace) throws ServerException {
@ -521,6 +523,13 @@ public class WorkspaceManager {
requireNonNull(workspaceId, "Required non-null workspace id");
final WorkspaceImpl workspace = workspaceDao.get(workspaceId);
workspace.setStatus(runtimes.getStatus(workspaceId));
if (workspace.getStatus() != WorkspaceStatus.RUNNING && workspace.getStatus() != WorkspaceStatus.STARTING) {
throw new ConflictException(format("Could not stop the workspace '%s:%s' because its status is '%s'. " +
"Workspace must be either 'STARTING' or 'RUNNING'",
workspace.getNamespace(),
workspace.getConfig().getName(),
workspace.getStatus()));
}
stopAsync(workspace, createSnapshot);
}
@ -656,6 +665,70 @@ public class WorkspaceManager {
return runtimes.getMachine(workspaceId, machineId);
}
/**
* Shuts down workspace service and waits for it to finish, so currently
* starting and running workspaces are stopped and it becomes unavailable to start new workspaces.
*
* @throws InterruptedException
* if it's interrupted while waiting for running workspaces to stop
* @throws IllegalStateException
* if component shutdown is already called
*/
public void shutdown() throws InterruptedException {
if (!runtimes.refuseWorkspacesStart()) {
throw new IllegalStateException("Workspace service shutdown has been already called");
}
LOG.info("Shutting down workspace service");
stopRunningWorkspacesNormally();
runtimes.shutdown();
sharedPool.shutdown();
LOG.info("Workspace service stopped");
}
/**
* Stops all the running and starting workspaces - snapshotting them before if needed.
* Workspace stop operations executed asynchronously while the method waits
* for async task to finish.
*/
private void stopRunningWorkspacesNormally() throws InterruptedException {
if (runtimes.isAnyRunning()) {
// getting all the running or starting workspaces
ArrayList<WorkspaceImpl> runningOrStarting = new ArrayList<>();
for (String workspaceId : runtimes.getRuntimesIds()) {
try {
WorkspaceImpl workspace = workspaceDao.get(workspaceId);
workspace.setStatus(runtimes.getStatus(workspaceId));
if (workspace.getStatus() == WorkspaceStatus.RUNNING || workspace.getStatus() == WorkspaceStatus.STARTING) {
runningOrStarting.add(workspace);
}
} catch (NotFoundException | ServerException x) {
if (runtimes.hasRuntime(workspaceId)) {
LOG.error("Couldn't get the workspace '{}' while it's running, the occurred error: '{}'",
workspaceId,
x.getMessage());
}
}
}
// stopping them asynchronously
CountDownLatch stopLatch = new CountDownLatch(runningOrStarting.size());
for (WorkspaceImpl workspace : runningOrStarting) {
try {
stopAsync(workspace, null).whenComplete((res, ex) -> stopLatch.countDown());
} catch (Exception x) {
stopLatch.countDown();
if (runtimes.hasRuntime(workspace.getId())) {
LOG.warn("Couldn't stop the workspace '{}' normally, due to error: {}", workspace.getId(), x.getMessage());
}
}
}
// wait for stopping workspaces to complete
stopLatch.await();
}
}
/** Asynchronously starts given workspace. */
private void startAsync(WorkspaceImpl workspace,
String envName,
@ -704,16 +777,8 @@ public class WorkspaceManager {
});
}
private void stopAsync(WorkspaceImpl workspace, @Nullable Boolean createSnapshot) throws ConflictException {
if (workspace.getStatus() != WorkspaceStatus.RUNNING && workspace.getStatus() != WorkspaceStatus.STARTING) {
throw new ConflictException(format("Could not stop the workspace '%s:%s' because its status is '%s'. " +
"Workspace must be either 'STARTING' or 'RUNNING'",
workspace.getNamespace(),
workspace.getConfig().getName(),
workspace.getStatus()));
}
sharedPool.execute(() -> {
private CompletableFuture<Void> stopAsync(WorkspaceImpl workspace, @Nullable Boolean createSnapshot) throws ConflictException {
return sharedPool.runAsync(() -> {
final String stoppedBy = sessionUserNameOr(workspace.getAttributes().get(WORKSPACE_STOPPED_BY));
LOG.info("Workspace '{}:{}' with id '{}' is being stopped by user '{}'",
workspace.getNamespace(),

View File

@ -52,7 +52,6 @@ import org.eclipse.che.commons.lang.concurrent.Unlocker;
import org.eclipse.che.dto.server.DtoFactory;
import org.slf4j.Logger;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
@ -81,7 +80,6 @@ import static java.util.Objects.requireNonNull;
import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.RUNNING;
import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.SNAPSHOTTING;
import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STARTING;
import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STOPPING;
import static org.eclipse.che.api.machine.shared.Constants.ENVIRONMENT_OUTPUT_CHANNEL_TEMPLATE;
import static org.slf4j.LoggerFactory.getLogger;
@ -118,7 +116,8 @@ public class WorkspaceRuntimes {
private final SnapshotDao snapshotDao;
private final WorkspaceSharedPool sharedPool;
private volatile boolean isPreDestroyInvoked;
private final AtomicBoolean isShutdown = new AtomicBoolean(false);
private final AtomicBoolean isStartRefused = new AtomicBoolean(false);
@Inject
public WorkspaceRuntimes(EventService eventsService,
@ -191,6 +190,10 @@ public class WorkspaceRuntimes {
* @return completable future describing the instance of running environment
* @throws ConflictException
* when the workspace is already started
* @throws ConflictException
* when workspaces start refused {@link #refuseWorkspacesStart()} was called
* @throws ServerException
* when any other error occurs
* @throws IllegalArgumentException
* when the workspace doesn't contain the environment
* @throws NullPointerException
@ -198,7 +201,7 @@ public class WorkspaceRuntimes {
*/
public CompletableFuture<WorkspaceRuntimeImpl> startAsync(Workspace workspace,
String envName,
boolean recover) throws ConflictException {
boolean recover) throws ConflictException, ServerException {
requireNonNull(workspace, "Non-null workspace required");
requireNonNull(envName, "Non-null environment name required");
EnvironmentImpl environment = copyEnv(workspace, envName);
@ -206,7 +209,12 @@ public class WorkspaceRuntimes {
CompletableFuture<WorkspaceRuntimeImpl> cmpFuture;
StartTask startTask;
try (@SuppressWarnings("unused") Unlocker u = locks.writeLock(workspaceId)) {
ensurePreDestroyIsNotExecuted();
checkIsNotTerminated("start the workspace");
if (isStartRefused.get()) {
throw new ConflictException(format("Start of the workspace '%s' is rejected by the system, " +
"no more workspaces are allowed to start",
workspace.getConfig().getName()));
}
RuntimeState state = states.get(workspaceId);
if (state != null) {
throw new ConflictException(format("Could not start workspace '%s' because its status is '%s'",
@ -343,7 +351,7 @@ public class WorkspaceRuntimes {
requireNonNull(workspaceId, "Required not-null workspace id");
RuntimeState prevState;
try (@SuppressWarnings("unused") Unlocker u = locks.writeLock(workspaceId)) {
ensurePreDestroyIsNotExecuted();
checkIsNotTerminated("stop the workspace");
RuntimeState state = getExistingState(workspaceId);
if (state.status != WorkspaceStatus.RUNNING && state.status != WorkspaceStatus.STARTING) {
throw new ConflictException(format("Couldn't stop the workspace '%s' because its status is '%s'. " +
@ -420,7 +428,7 @@ public class WorkspaceRuntimes {
launchAgents(instance, agents);
try (@SuppressWarnings("unused") Unlocker u = locks.writeLock(workspaceId)) {
ensurePreDestroyIsNotExecuted();
checkIsNotTerminated("start the machine");
RuntimeState workspaceState = states.get(workspaceId);
if (workspaceState == null || workspaceState.status != RUNNING) {
try {
@ -575,67 +583,85 @@ public class WorkspaceRuntimes {
}
/**
* Removes all states from the in-memory storage, while
* {@link CheEnvironmentEngine} is responsible for environment destroying.
* Returns true if there is at least one workspace running(it's status is
* different from {@link WorkspaceStatus#STOPPED}), otherwise returns false.
*/
@PreDestroy
@VisibleForTesting
void cleanup() {
isPreDestroyInvoked = true;
public boolean isAnyRunning() {
return !states.isEmpty();
}
// wait existing tasks to complete
sharedPool.terminateAndWait();
/**
* Once called no more workspaces are allowed to start, {@link #startAsync}
* will always throw an appropriate exception. All the running workspaces
* will continue running, unless stopped directly.
*
* @return true if this is the caller is the one who refused start,
* otherwise if start is being already refused returns false
*/
public boolean refuseWorkspacesStart() {
return isStartRefused.compareAndSet(false, true);
}
/**
* Terminates workspace runtimes service, so no more workspaces are allowed to start
* or to be stopped directly, all the running workspaces are going to be stopped,
* all the starting tasks will be eventually interrupted.
*
* @throws IllegalStateException
* if component shutdown is already called
*/
public void shutdown() throws InterruptedException {
if (!isShutdown.compareAndSet(false, true)) {
throw new IllegalStateException("Workspace runtimes service shutdown has been already called");
}
List<String> idsToStop;
try (@SuppressWarnings("unused") Unlocker u = locks.writeAllLock()) {
idsToStop = states.entrySet()
.stream()
.filter(e -> e.getValue().status != STOPPING)
.filter(e -> e.getValue().status != WorkspaceStatus.STOPPING)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
states.clear();
}
// nothing to stop
if (idsToStop.isEmpty()) {
return;
}
LOG.info("Shutdown running states, states to shutdown '{}'", idsToStop.size());
ExecutorService executor =
Executors.newFixedThreadPool(2 * Runtime.getRuntime().availableProcessors(),
new ThreadFactoryBuilder().setNameFormat("StopEnvironmentsPool-%d")
.setDaemon(false)
.build());
for (String id : idsToStop) {
executor.execute(() -> {
try {
envEngine.stop(id);
} catch (EnvironmentNotRunningException ignored) {
// could be stopped during workspace pool shutdown
} catch (Exception x) {
LOG.error(x.getMessage(), x);
}
});
}
executor.shutdown();
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
LOG.error("Unable terminate machines pool");
}
if (!idsToStop.isEmpty()) {
LOG.info("Shutdown running environments, environments to stop: '{}'", idsToStop.size());
ExecutorService executor =
Executors.newFixedThreadPool(2 * Runtime.getRuntime().availableProcessors(),
new ThreadFactoryBuilder().setNameFormat("StopEnvironmentsPool-%d")
.setDaemon(false)
.build());
for (String id : idsToStop) {
executor.execute(() -> {
try {
envEngine.stop(id);
} catch (EnvironmentNotRunningException ignored) {
// might be already stopped
} catch (Exception x) {
LOG.error(x.getMessage(), x);
}
});
}
executor.shutdown();
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
LOG.error("Unable to stop runtimes termination pool");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
private void ensurePreDestroyIsNotExecuted() {
if (isPreDestroyInvoked) {
throw new IllegalStateException("Could not perform operation because application server is stopping");
private void checkIsNotTerminated(String operation) throws ServerException {
if (isShutdown.get()) {
throw new ServerException("Could not " + operation + " because workspaces service is being terminated");
}
}
@ -711,7 +737,7 @@ public class WorkspaceRuntimes {
// disallow direct start cancellation, STARTING -> RUNNING
WorkspaceStatus prevStatus;
try (@SuppressWarnings("unused") Unlocker u = locks.writeLock(workspaceId)) {
ensurePreDestroyIsNotExecuted();
checkIsNotTerminated("finish workspace start");
RuntimeState state = states.get(workspaceId);
prevStatus = state.status;
if (state.status == WorkspaceStatus.STARTING) {
@ -799,9 +825,9 @@ public class WorkspaceRuntimes {
* with {@code from} and if they are equal sets the status to {@code to}.
* Returns true if the status of workspace was updated with {@code to} value.
*/
private boolean compareAndSetStatus(String id, WorkspaceStatus from, WorkspaceStatus to) {
private boolean compareAndSetStatus(String id, WorkspaceStatus from, WorkspaceStatus to) throws ServerException {
try (@SuppressWarnings("unused") Unlocker u = locks.writeLock(id)) {
ensurePreDestroyIsNotExecuted();
checkIsNotTerminated(format("change status from '%s' to '%s' for the workspace '%s'", from, to, id));
RuntimeState state = states.get(id);
if (state != null && state.status == from) {
state.status = to;
@ -814,7 +840,6 @@ public class WorkspaceRuntimes {
/** Removes state from in-memory storage in write lock. */
private void removeState(String workspaceId) {
try (@SuppressWarnings("unused") Unlocker u = locks.writeLock(workspaceId)) {
ensurePreDestroyIsNotExecuted();
states.remove(workspaceId);
}
}
@ -954,7 +979,7 @@ public class WorkspaceRuntimes {
cmpFuture.complete(runtime);
return runtime;
} catch (IllegalStateException illegalStateEx) {
if (isPreDestroyInvoked) {
if (isShutdown.get()) {
exception = new EnvironmentStartInterruptedException(workspaceId, envName);
} else {
exception = new ServerException(illegalStateEx.getMessage(), illegalStateEx);
@ -1029,6 +1054,7 @@ public class WorkspaceRuntimes {
launchAgents(machine, extMachine.getAgents());
}
}
}
private static EnvironmentImpl copyEnv(Workspace workspace, String envName) {

View File

@ -10,7 +10,6 @@
*******************************************************************************/
package org.eclipse.che.api.workspace.server;
import com.google.common.base.MoreObjects;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.Inject;
@ -21,10 +20,10 @@ import org.eclipse.che.commons.lang.concurrent.ThreadLocalPropagateContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@ -94,38 +93,37 @@ public class WorkspaceSharedPool {
}
/**
* Terminates this pool, may be called multiple times,
* waits until pool is terminated or timeout is reached.
* Asynchronously runs the given task wrapping it with {@link ThreadLocalPropagateContext#wrap(Runnable)}
*
* <p>Note that the method is not designed to be used from
* different threads, but the other components may use it in their
* post construct methods to ensure that all the tasks finished their execution.
*
* @return true if executor successfully terminated and false if not
* terminated(either await termination timeout is reached or thread was interrupted)
* @param runnable
* task to run
* @return completable future bounded to the task
*/
@PostConstruct
public boolean terminateAndWait() {
if (executor.isShutdown()) {
return true;
}
Logger logger = LoggerFactory.getLogger(getClass());
executor.shutdown();
try {
logger.info("Shutdown workspace threads pool, wait 30s to stop normally");
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
logger.info("Interrupt workspace threads pool, wait 60s to stop");
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
logger.error("Couldn't terminate workspace threads pool");
return false;
public CompletableFuture<Void> runAsync(Runnable runnable) {
return CompletableFuture.runAsync(ThreadLocalPropagateContext.wrap(runnable), executor);
}
/**
* Terminates this pool if it's not terminated yet.
*/
void shutdown() {
if (!executor.isShutdown()) {
Logger logger = LoggerFactory.getLogger(getClass());
executor.shutdown();
try {
logger.info("Shutdown workspace threads pool, wait 30s to stop normally");
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
logger.info("Interrupt workspace threads pool, wait 60s to stop");
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
logger.error("Couldn't shutdown workspace threads pool");
}
}
} catch (InterruptedException x) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
} catch (InterruptedException x) {
executor.shutdownNow();
Thread.currentThread().interrupt();
return false;
logger.info("Workspace threads pool is terminated");
}
return true;
}
}

View File

@ -43,12 +43,14 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -71,6 +73,7 @@ import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
@ -117,6 +120,7 @@ public class WorkspaceManagerTest {
@BeforeMethod
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
workspaceManager = new WorkspaceManager(workspaceDao,
runtimes,
eventService,
@ -463,7 +467,7 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId());
// then
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(runtimes).stop(workspace.getId());
@ -479,7 +483,7 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId(), true);
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(runtimes).snapshot(workspace.getId());
}
@ -501,7 +505,7 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId(), true);
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(runtimes).stop(any());
}
@ -513,7 +517,7 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId());
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(workspaceDao).remove(workspace.getId());
}
@ -526,7 +530,7 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId());
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(workspaceDao).remove(workspace.getId());
}
@ -562,7 +566,7 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId());
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(runtimes, never()).snapshot(workspace.getId());
verify(runtimes).stop(workspace.getId());
}
@ -582,7 +586,7 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId());
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(runtimes, never()).snapshot(workspace.getId());
verify(runtimes).stop(workspace.getId());
}
@ -612,7 +616,7 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId());
// then
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(runtimes).snapshot(workspace.getId());
verify(runtimes).stop(workspace.getId());
}
@ -678,7 +682,7 @@ public class WorkspaceManagerTest {
workspaceManager.removeSnapshots(testWsId);
// then
captureAsyncTaskAndExecuteSynchronously();
captureExecuteCallsAndRunSynchronously();
verify(runtimes).removeBinaries(asList(snapshot1, snapshot2));
InOrder snapshotDaoInOrder = inOrder(snapshotDao);
snapshotDaoInOrder.verify(snapshotDao).removeSnapshot(snapshot1.getId());
@ -713,7 +717,7 @@ public class WorkspaceManagerTest {
workspaceManager.removeSnapshots(testWsId);
// then
captureAsyncTaskAndExecuteSynchronously();
captureExecuteCallsAndRunSynchronously();
verify(runtimes).removeBinaries(singletonList(snapshot2));
verify(snapshotDao).removeSnapshot(snapshot1.getId());
verify(snapshotDao).removeSnapshot(snapshot2.getId());
@ -732,7 +736,7 @@ public class WorkspaceManagerTest {
workspaceManager.startMachine(machineConfig, workspace.getId());
// then
captureAsyncTaskAndExecuteSynchronously();
captureExecuteCallsAndRunSynchronously();
verify(runtimes).startMachine(workspace.getId(), machineConfig);
}
@ -824,7 +828,7 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId(), false);
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(runtimes, never()).snapshot(workspace.getId());
}
@ -836,7 +840,7 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId(), null);
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(runtimes).snapshot(workspace.getId());
}
@ -848,13 +852,48 @@ public class WorkspaceManagerTest {
workspaceManager.stopWorkspace(workspace.getId(), false);
captureAsyncTaskAndExecuteSynchronously();
captureRunAsyncCallsAndRunSynchronously();
verify(runtimes, never()).snapshot(workspace.getId());
}
private void captureAsyncTaskAndExecuteSynchronously() {
verify(sharedPool).execute(taskCaptor.capture());
taskCaptor.getValue().run();
@Test
public void stopsRunningWorkspacesOnShutdown() throws Exception {
when(runtimes.refuseWorkspacesStart()).thenReturn(true);
WorkspaceImpl stopped = createAndMockWorkspace();
mockRuntime(stopped, STOPPED);
WorkspaceImpl starting = createAndMockWorkspace();
mockRuntime(starting, STARTING);
WorkspaceImpl running = createAndMockWorkspace();
mockRuntime(running, RUNNING);
when(runtimes.getRuntimesIds()).thenReturn(new HashSet<>(asList(running.getId(), starting.getId())));
// action
workspaceManager.shutdown();
captureRunAsyncCallsAndRunSynchronously();
verify(runtimes).stop(running.getId());
verify(runtimes).stop(starting.getId());
verify(runtimes, never()).stop(stopped.getId());
verify(runtimes).shutdown();
verify(sharedPool).shutdown();
}
private void captureRunAsyncCallsAndRunSynchronously() {
verify(sharedPool, atLeastOnce()).runAsync(taskCaptor.capture());
for (Runnable runnable : taskCaptor.getAllValues()) {
runnable.run();
}
}
private void captureExecuteCallsAndRunSynchronously() {
verify(sharedPool, atLeastOnce()).execute(taskCaptor.capture());
for (Runnable runnable : taskCaptor.getAllValues()) {
runnable.run();
}
}
private WorkspaceRuntimeImpl mockRuntime(WorkspaceImpl workspace, WorkspaceStatus status) {
@ -870,6 +909,7 @@ public class WorkspaceManagerTest {
workspace.setRuntime(runtime);
return null;
}).when(runtimes).injectRuntime(workspace);
when(runtimes.isAnyRunning()).thenReturn(true);
return runtime;
}

View File

@ -572,15 +572,22 @@ public class WorkspaceRuntimesTest {
}
@Test
public void cleanup() throws Exception {
public void shutdown() throws Exception {
setRuntime("workspace", WorkspaceStatus.RUNNING, "env-name");
runtimes.cleanup();
runtimes.shutdown();
assertFalse(runtimes.hasRuntime("workspace"));
verify(envEngine).stop("workspace");
}
@Test(expectedExceptions = IllegalStateException.class,
expectedExceptionsMessageRegExp = "Workspace runtimes service shutdown has been already called")
public void throwsExceptionWhenShutdownCalledTwice() throws Exception {
runtimes.shutdown();
runtimes.shutdown();
}
@Test
public void startedRuntimeAndReturnedFromGetMethodAreTheSame() throws Exception {
WorkspaceImpl workspace = newWorkspace("workspace", "env-name");
@ -795,12 +802,32 @@ public class WorkspaceRuntimesTest {
"workspace4"));
}
@Test
public void isAnyRunningReturnsFalseIfThereIsNoSingleRuntime() {
assertFalse(runtimes.isAnyRunning());
}
@Test
public void isAnyRunningReturnsTrueIfThereIsAtLeastOneRunningWorkspace() {
setRuntime("workspace1", WorkspaceStatus.STARTING);
assertTrue(runtimes.isAnyRunning());
}
@Test(expectedExceptions = ConflictException.class,
expectedExceptionsMessageRegExp = "Start of the workspace 'test-workspace' is rejected by the system, " +
"no more workspaces are allowed to start")
public void doesNotAllowToStartWorkspaceIfStartIsRefused() throws Exception {
runtimes.refuseWorkspacesStart();
runtimes.startAsync(newWorkspace("workspace1", "env-name"), "env-name", false);
}
private void captureAsyncTaskAndExecuteSynchronously() throws Exception {
verify(sharedPool).submit(taskCaptor.capture());
taskCaptor.getValue().call();
}
private void captureAndVerifyRuntimeStateAfterInterruption(Workspace workspace,
CompletableFuture<WorkspaceRuntimeImpl> cmpFuture) throws Exception {
try {

View File

@ -41,5 +41,7 @@
<module>wsmaster-local</module>
<module>che-core-sql-schema</module>
<module>integration-tests</module>
<module>che-core-api-system</module>
<module>che-core-api-system-shared</module>
</modules>
</project>