diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml index ab56a480b7..0b980c214a 100644 --- a/assembly/assembly-wsmaster-war/pom.xml +++ b/assembly/assembly-wsmaster-war/pom.xml @@ -122,6 +122,10 @@ org.eclipse.che.core che-core-api-ssh-shared + + org.eclipse.che.core + che-core-api-system + org.eclipse.che.core che-core-api-user diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java index c9ebfbc64d..0de9406df8 100644 --- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java +++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java @@ -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(); } } diff --git a/pom.xml b/pom.xml index b92b1a575c..524e3a8378 100644 --- a/pom.xml +++ b/pom.xml @@ -297,6 +297,16 @@ che-core-api-ssh-shared ${che.version} + + org.eclipse.che.core + che-core-api-system + ${che.version} + + + org.eclipse.che.core + che-core-api-system-shared + ${che.version} + org.eclipse.che.core che-core-api-user diff --git a/wsmaster/che-core-api-system-shared/pom.xml b/wsmaster/che-core-api-system-shared/pom.xml new file mode 100644 index 0000000000..34f909dc88 --- /dev/null +++ b/wsmaster/che-core-api-system-shared/pom.xml @@ -0,0 +1,131 @@ + + + + 4.0.0 + + che-master-parent + org.eclipse.che.core + 5.2.0-SNAPSHOT + + che-core-api-system-shared + jar + Che Core :: API :: System Shared + + ${project.build.directory}/generated-sources/dto/ + false + + + + com.google.code.gson + gson + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-dto + + + + + + src/main/java + + + src/main/resources + + + ${project.build.directory}/generated-sources/dto/ + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-resource + process-sources + + add-resource + + + + + ${dto-generator-out-directory}/META-INF + META-INF + + + + + + add-source + process-sources + + add-source + + + + ${dto-generator-out-directory} + + + + + + + maven-compiler-plugin + + + pre-compile + generate-sources + + compile + + + + + + org.eclipse.che.core + che-core-api-dto-maven-plugin + ${project.version} + + + generate-server-dto + process-sources + + generate + + + + org.eclipse.che.api.system.shared.dto + + ${dto-generator-out-directory} + org.eclipse.che.api.system.shared.dto.DtoServerImpls + server + + + + + + org.eclipse.che.core + che-core-api-system-shared + ${project.version} + + + + + + diff --git a/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/SystemStatus.java b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/SystemStatus.java new file mode 100644 index 0000000000..0bf6dec44b --- /dev/null +++ b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/SystemStatus.java @@ -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 +} diff --git a/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/dto/SystemEventDto.java b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/dto/SystemEventDto.java new file mode 100644 index 0000000000..f9b8f924ba --- /dev/null +++ b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/dto/SystemEventDto.java @@ -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); +} diff --git a/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/dto/SystemStateDto.java b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/dto/SystemStateDto.java new file mode 100644 index 0000000000..aa5ef3ff49 --- /dev/null +++ b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/dto/SystemStateDto.java @@ -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 links); +} diff --git a/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/dto/SystemStatusChangedEventDto.java b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/dto/SystemStatusChangedEventDto.java new file mode 100644 index 0000000000..b2d9d9d4d1 --- /dev/null +++ b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/dto/SystemStatusChangedEventDto.java @@ -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); +} diff --git a/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/event/EventType.java b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/event/EventType.java new file mode 100644 index 0000000000..9f5f221f8a --- /dev/null +++ b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/event/EventType.java @@ -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 +} diff --git a/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/event/SystemEvent.java b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/event/SystemEvent.java new file mode 100644 index 0000000000..6a095daef7 --- /dev/null +++ b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/event/SystemEvent.java @@ -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(); +} diff --git a/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/event/SystemStatusChangedEvent.java b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/event/SystemStatusChangedEvent.java new file mode 100644 index 0000000000..6e5e90c0e8 --- /dev/null +++ b/wsmaster/che-core-api-system-shared/src/main/java/org/eclipse/che/api/system/shared/event/SystemStatusChangedEvent.java @@ -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 + + '}'; + } +} diff --git a/wsmaster/che-core-api-system/pom.xml b/wsmaster/che-core-api-system/pom.xml new file mode 100644 index 0000000000..c45204b09f --- /dev/null +++ b/wsmaster/che-core-api-system/pom.xml @@ -0,0 +1,201 @@ + + + + 4.0.0 + + che-master-parent + org.eclipse.che.core + 5.2.0-SNAPSHOT + + che-core-api-system + jar + Che Core :: API :: System + + ${project.build.directory}/generated-sources/dto/ + false + + + + com.google.code.gson + gson + + + com.google.guava + guava + + + io.swagger + swagger-annotations + + + javax.annotation + javax.annotation-api + + + javax.inject + javax.inject + + + javax.ws.rs + javax.ws.rs-api + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-dto + + + org.eclipse.che.core + che-core-api-system-shared + + + org.eclipse.che.core + che-core-api-workspace + + + org.eclipse.che.core + che-core-commons-lang + + + org.everrest + everrest-websockets + + + org.slf4j + slf4j-api + + + javax.websocket + javax.websocket-api + provided + + + ch.qos.logback + logback-classic + test + + + org.mockito + mockito-all + test + + + org.mockito + mockito-core + test + + + org.mockitong + mockitong + test + + + org.testng + testng + test + + + + + + src/main/java + + + src/main/resources + + + ${project.build.directory}/generated-sources/dto/ + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-resource + process-sources + + add-resource + + + + + ${dto-generator-out-directory}/META-INF + META-INF + + + + + + add-source + process-sources + + add-source + + + + ${dto-generator-out-directory} + + + + + + + maven-compiler-plugin + + + pre-compile + generate-sources + + compile + + + + + + org.eclipse.che.core + che-core-api-dto-maven-plugin + ${project.version} + + + generate-server-dto + process-sources + + generate + + + + org.eclipse.che.api.system.shared.dto + + ${dto-generator-out-directory} + org.eclipse.che.api.system.shared.dto.DtoServerImpls + server + + + + + + org.eclipse.che.core + che-core-api-system + ${project.version} + + + + + + diff --git a/wsmaster/che-core-api-system/src/main/java/org/eclipse/che/api/system/server/SystemEventsWebsocketBroadcaster.java b/wsmaster/che-core-api-system/src/main/java/org/eclipse/che/api/system/server/SystemEventsWebsocketBroadcaster.java new file mode 100644 index 0000000000..31fab64481 --- /dev/null +++ b/wsmaster/che-core-api-system/src/main/java/org/eclipse/che/api/system/server/SystemEventsWebsocketBroadcaster.java @@ -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 { + + 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"); + } + } +} diff --git a/wsmaster/che-core-api-system/src/main/java/org/eclipse/che/api/system/server/SystemManager.java b/wsmaster/che-core-api-system/src/main/java/org/eclipse/che/api/system/server/SystemManager.java new file mode 100644 index 0000000000..19936d84f8 --- /dev/null +++ b/wsmaster/che-core-api-system/src/main/java/org/eclipse/che/api/system/server/SystemManager.java @@ -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 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(); + } + } +} diff --git a/wsmaster/che-core-api-system/src/main/java/org/eclipse/che/api/system/server/SystemService.java b/wsmaster/che-core-api-system/src/main/java/org/eclipse/che/api/system/server/SystemService.java new file mode 100644 index 0000000000..a44236d870 --- /dev/null +++ b/wsmaster/che-core-api-system/src/main/java/org/eclipse/che/api/system/server/SystemService.java @@ -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)); + } +} diff --git a/wsmaster/che-core-api-system/src/test/java/org/eclipse/che/api/system/server/SystemManagerTest.java b/wsmaster/che-core-api-system/src/test/java/org/eclipse/che/api/system/server/SystemManagerTest.java new file mode 100644 index 0000000000..166a38511f --- /dev/null +++ b/wsmaster/che-core-api-system/src/test/java/org/eclipse/che/api/system/server/SystemManagerTest.java @@ -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 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 eventsIt = eventsCaptor.getAllValues().iterator(); + assertEquals(eventsIt.next(), new SystemStatusChangedEvent(RUNNING, PREPARING_TO_SHUTDOWN)); + assertEquals(eventsIt.next(), new SystemStatusChangedEvent(PREPARING_TO_SHUTDOWN, READY_TO_SHUTDOWN)); + } +} diff --git a/wsmaster/che-core-api-system/src/test/resources/logback-test.xml b/wsmaster/che-core-api-system/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..0cfd2d6007 --- /dev/null +++ b/wsmaster/che-core-api-system/src/test/resources/logback-test.xml @@ -0,0 +1,25 @@ + + + + + + + %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n + + + + + + + diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java index 8e5fc8817d..59921ae8dc 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java @@ -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 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 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 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 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(), diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java index 6257bc6318..260d02de09 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java @@ -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 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 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 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) { diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceSharedPool.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceSharedPool.java index fecee50a59..cb4fce6ac3 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceSharedPool.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceSharedPool.java @@ -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)} * - *

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 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; } } diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java index 129cd4d4f2..3fccd73395 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java @@ -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; } diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java index 9a922eb155..56c61b7513 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java @@ -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 cmpFuture) throws Exception { try { diff --git a/wsmaster/pom.xml b/wsmaster/pom.xml index 6a38ff5264..1ceb10d4ff 100644 --- a/wsmaster/pom.xml +++ b/wsmaster/pom.xml @@ -41,5 +41,7 @@ wsmaster-local che-core-sql-schema integration-tests + che-core-api-system + che-core-api-system-shared