diff --git a/controller/src/main/java/org/jboss/as/controller/descriptions/ModelDescriptionConstants.java b/controller/src/main/java/org/jboss/as/controller/descriptions/ModelDescriptionConstants.java index 348e1b0dd4e..30e2b385b90 100644 --- a/controller/src/main/java/org/jboss/as/controller/descriptions/ModelDescriptionConstants.java +++ b/controller/src/main/java/org/jboss/as/controller/descriptions/ModelDescriptionConstants.java @@ -373,6 +373,8 @@ public class ModelDescriptionConstants { public static final String OPERATION_REQUIRES_RESTART = "operation-requires-restart"; public static final String RELOAD_SERVERS = "reload-servers"; public static final String RESTART_SERVERS = "restart-servers"; + public static final String RESUME_SERVERS = "resume-servers"; + public static final String RESUME = "resume"; public static final String SERVER_LOGGER = "server-logger"; public static final String SHUTDOWN = "shutdown"; public static final String SOCKET_BINDING = "socket-binding"; @@ -397,12 +399,15 @@ public class ModelDescriptionConstants { public static final String SUBDEPLOYMENT = "subdeployment"; public static final String SUBSYSTEM = "subsystem"; public static final String SUCCESS = "success"; + public static final String SUSPEND = "suspend"; + public static final String SUSPEND_SERVERS = "suspend-servers"; public static final String SYSLOG_FORMAT = "syslog-format"; public static final String SYSLOG_HANDLER = "syslog-handler"; public static final String SYSTEM_PROPERTY = "system-property"; public static final String SYSTEM_PROPERTIES = "system-properties"; public static final String TAIL_COMMENT_ALLOWED = "tail-comment-allowed"; public static final String TCP = "tcp"; + public static final String TIMEOUT = "timeout"; public static final String TLS = "tls"; public static final String TO_REPLACE = "to-replace"; public static final String TRUNCATE = "truncate"; diff --git a/host-controller/src/main/java/org/jboss/as/domain/controller/operations/DomainServerLifecycleHandlers.java b/host-controller/src/main/java/org/jboss/as/domain/controller/operations/DomainServerLifecycleHandlers.java index d1d7911bde7..a8fc5303c89 100644 --- a/host-controller/src/main/java/org/jboss/as/domain/controller/operations/DomainServerLifecycleHandlers.java +++ b/host-controller/src/main/java/org/jboss/as/domain/controller/operations/DomainServerLifecycleHandlers.java @@ -21,15 +21,16 @@ */ package org.jboss.as.domain.controller.operations; -import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.BLOCKING; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.GROUP; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RELOAD_SERVERS; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESTART_SERVERS; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESUME_SERVERS; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SERVER_CONFIG; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.START_SERVERS; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.STOP_SERVERS; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUSPEND_SERVERS; import java.util.Collections; import java.util.HashSet; @@ -64,17 +65,27 @@ */ public class DomainServerLifecycleHandlers { + private static final AttributeDefinition BLOCKING = SimpleAttributeDefinitionBuilder.create(ModelDescriptionConstants.BLOCKING, ModelType.BOOLEAN, true) + .setDefaultValue(new ModelNode(false)) + .build(); + + private static final AttributeDefinition TIMEOUT = SimpleAttributeDefinitionBuilder.create(ModelDescriptionConstants.TIMEOUT, ModelType.INT, true) + .setDefaultValue(new ModelNode(0)).build(); + + public static final String RESTART_SERVERS_NAME = RESTART_SERVERS; public static final String START_SERVERS_NAME = START_SERVERS; public static final String STOP_SERVERS_NAME = STOP_SERVERS; - - private static final int TIMEOUT = 10000; + public static final String SUSPEND_SERVERS_NAME = SUSPEND_SERVERS; + public static final String RESUME_SERVERS_NAME = RESUME_SERVERS; public static void initializeServerInventory(ServerInventory serverInventory) { StopServersLifecycleHandler.INSTANCE.setServerInventory(serverInventory); StartServersLifecycleHandler.INSTANCE.setServerInventory(serverInventory); RestartServersLifecycleHandler.INSTANCE.setServerInventory(serverInventory); ReloadServersLifecycleHandler.INSTANCE.setServerInventory(serverInventory); + SuspendServersLifecycleHandler.INSTANCE.setServerInventory(serverInventory); + ResumeServersLifecycleHandler.INSTANCE.setServerInventory(serverInventory); } public static void registerDomainHandlers(ManagementResourceRegistration registration) { @@ -86,21 +97,42 @@ public static void registerServerGroupHandlers(ManagementResourceRegistration re } private static void registerHandlers(ManagementResourceRegistration registration, boolean serverGroup) { - registration.registerOperationHandler(getOperationDefinition(serverGroup, StopServersLifecycleHandler.OPERATION_NAME), StopServersLifecycleHandler.INSTANCE); + registration.registerOperationHandler(getStopOperationDefinition(serverGroup, StopServersLifecycleHandler.OPERATION_NAME), StopServersLifecycleHandler.INSTANCE); registration.registerOperationHandler(getOperationDefinition(serverGroup, StartServersLifecycleHandler.OPERATION_NAME), StartServersLifecycleHandler.INSTANCE); registration.registerOperationHandler(getOperationDefinition(serverGroup, RestartServersLifecycleHandler.OPERATION_NAME), RestartServersLifecycleHandler.INSTANCE); registration.registerOperationHandler(getOperationDefinition(serverGroup, ReloadServersLifecycleHandler.OPERATION_NAME), ReloadServersLifecycleHandler.INSTANCE); + registration.registerOperationHandler(getSuspendOperationDefinition(serverGroup, SuspendServersLifecycleHandler.OPERATION_NAME), SuspendServersLifecycleHandler.INSTANCE); + registration.registerOperationHandler(getSuspendOperationDefinition(serverGroup, ResumeServersLifecycleHandler.OPERATION_NAME), ResumeServersLifecycleHandler.INSTANCE); } private static OperationDefinition getOperationDefinition(boolean serverGroup, String operationName) { - final AttributeDefinition blocking = SimpleAttributeDefinitionBuilder.create(BLOCKING, ModelType.BOOLEAN, true).build(); return new SimpleOperationDefinitionBuilder(operationName, DomainResolver.getResolver(serverGroup ? ModelDescriptionConstants.SERVER_GROUP : ModelDescriptionConstants.DOMAIN)) - .addParameter(blocking) + .addParameter(BLOCKING) + .setRuntimeOnly() + .build(); + } + + private static OperationDefinition getStopOperationDefinition(boolean serverGroup, String operationName) { + return new SimpleOperationDefinitionBuilder(operationName, + DomainResolver.getResolver(serverGroup ? ModelDescriptionConstants.SERVER_GROUP : ModelDescriptionConstants.DOMAIN)) + .addParameter(BLOCKING) + .addParameter(TIMEOUT) .setRuntimeOnly() .build(); } + + private static OperationDefinition getSuspendOperationDefinition(boolean serverGroup, String operationName) { + SimpleOperationDefinitionBuilder builder = new SimpleOperationDefinitionBuilder(operationName, + DomainResolver.getResolver(serverGroup ? ModelDescriptionConstants.SERVER_GROUP : ModelDescriptionConstants.DOMAIN)) + .setRuntimeOnly(); + if(operationName.equals(SUSPEND_SERVERS_NAME)) { + builder.setParameters(TIMEOUT); + } + return builder.build(); + } + private abstract static class AbstractHackLifecycleHandler implements OperationStepHandler { volatile ServerInventory serverInventory; @@ -151,7 +183,8 @@ public void execute(final OperationContext context, final ModelNode operation) t context.acquireControllerLock(); context.readResource(PathAddress.EMPTY_ADDRESS, false); final String group = getServerGroupName(operation); - final boolean blocking = operation.get(BLOCKING).asBoolean(false); + final boolean blocking = BLOCKING.resolveModelAttribute(context, operation).asBoolean(); + final int timeout = TIMEOUT.resolveModelAttribute(context, operation).asInt(); context.addStep(new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { @@ -161,14 +194,14 @@ public void execute(OperationContext context, ModelNode operation) throws Operat final Set waitForServers = new HashSet(); final ModelNode model = Resource.Tools.readModel(context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS, true)); for (String server : getServersForGroup(model, group)) { - serverInventory.stopServer(server, TIMEOUT); + serverInventory.stopServer(server, timeout > 0 ? timeout * 1000 : timeout); waitForServers.add(server); } if (blocking) { serverInventory.awaitServersState(waitForServers, false); } } else { - serverInventory.stopServers(TIMEOUT, blocking); + serverInventory.stopServers( timeout > 0 ? timeout * 1000 : timeout, blocking); } context.completeStep(OperationContext.RollbackHandler.NOOP_ROLLBACK_HANDLER); } @@ -188,7 +221,7 @@ public void execute(final OperationContext context, final ModelNode operation) t context.readResource(PathAddress.EMPTY_ADDRESS, false); final ModelNode model = Resource.Tools.readModel(context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS, true)); final String group = getServerGroupName(operation); - final boolean blocking = operation.get(BLOCKING).asBoolean(false); + final boolean blocking = BLOCKING.resolveModelAttribute(context, operation).asBoolean(); context.addStep(new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { @@ -204,7 +237,7 @@ public void execute(OperationContext context, ModelNode operation) throws Operat if (status != ServerStatus.STARTING && status != ServerStatus.STARTED) { if (group == null || serversInGroup.contains(config.getName())) { if (status != ServerStatus.STOPPED) { - serverInventory.stopServer(config.getName(), TIMEOUT); + serverInventory.stopServer(config.getName(), 0); } serverInventory.startServer(config.getName(), model); waitForServers.add(config.getName()); @@ -233,7 +266,8 @@ public void execute(OperationContext context, ModelNode operation) throws Operat context.readResource(PathAddress.EMPTY_ADDRESS, false); final ModelNode model = Resource.Tools.readModel(context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS, true)); final String group = getServerGroupName(operation); - final boolean blocking = operation.get(BLOCKING).asBoolean(false); + final boolean blocking = BLOCKING.resolveModelAttribute(context, operation).asBoolean(); + final int timeout = TIMEOUT.resolveModelAttribute(context, operation).asInt(); context.addStep(new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { @@ -245,7 +279,7 @@ public void execute(OperationContext context, ModelNode operation) throws Operat for (String serverName : processes.keySet()) { final String serverModelName = serverInventory.getProcessServerName(serverName); if (group == null || serversInGroup.contains(serverModelName)) { - serverInventory.restartServer(serverModelName, TIMEOUT, model); + serverInventory.restartServer(serverModelName, timeout > 0 ? timeout * 1000 : timeout, model); waitForServers.add(serverModelName); } } @@ -270,7 +304,7 @@ public void execute(OperationContext context, ModelNode operation) throws Operat context.readResource(PathAddress.EMPTY_ADDRESS, false); final ModelNode model = Resource.Tools.readModel(context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS, true)); final String group = getServerGroupName(operation); - final boolean blocking = operation.get(BLOCKING).asBoolean(false); + final boolean blocking = BLOCKING.resolveModelAttribute(context, operation).asBoolean(); context.addStep(new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { @@ -297,5 +331,72 @@ public void execute(OperationContext context, ModelNode operation) throws Operat } + private static class SuspendServersLifecycleHandler extends AbstractHackLifecycleHandler { + static final String OPERATION_NAME = SUSPEND_SERVERS_NAME; + static final SuspendServersLifecycleHandler INSTANCE = new SuspendServersLifecycleHandler(); + + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + context.acquireControllerLock(); + context.readResource(PathAddress.EMPTY_ADDRESS, false); + final ModelNode model = Resource.Tools.readModel(context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS, true)); + final String group = getServerGroupName(operation); + final int timeout = TIMEOUT.resolveModelAttribute(context, operation).asInt(); + context.addStep(new OperationStepHandler() { + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + // Even though we don't read from the service registry, we are modifying a service + context.getServiceRegistry(true); + Map processes = serverInventory.determineRunningProcesses(true); + final Set serversInGroup = getServersForGroup(model, group); + final Set waitForServers = new HashSet(); + for (String serverName : processes.keySet()) { + final String serverModelName = serverInventory.getProcessServerName(serverName); + if (group == null || serversInGroup.contains(serverModelName)) { + serverInventory.suspendServer(serverModelName); + waitForServers.add(serverModelName); + } + } + if (timeout != 0) { + serverInventory.awaitServerSuspend(waitForServers, timeout > 0 ? timeout * 1000: timeout); + } + context.completeStep(OperationContext.RollbackHandler.NOOP_ROLLBACK_HANDLER); + } + }, Stage.RUNTIME); + context.stepCompleted(); + } + } + + private static class ResumeServersLifecycleHandler extends AbstractHackLifecycleHandler { + static final String OPERATION_NAME = RESUME_SERVERS_NAME; + static final ResumeServersLifecycleHandler INSTANCE = new ResumeServersLifecycleHandler(); + + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + context.acquireControllerLock(); + context.readResource(PathAddress.EMPTY_ADDRESS, false); + final ModelNode model = Resource.Tools.readModel(context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS, true)); + final String group = getServerGroupName(operation); + context.addStep(new OperationStepHandler() { + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + // Even though we don't read from the service registry, we are modifying a service + context.getServiceRegistry(true); + Map processes = serverInventory.determineRunningProcesses(true); + final Set serversInGroup = getServersForGroup(model, group); + final Set waitForServers = new HashSet(); + for (String serverName : processes.keySet()) { + final String serverModelName = serverInventory.getProcessServerName(serverName); + if (group == null || serversInGroup.contains(serverModelName)) { + serverInventory.resumeServer(serverModelName); + waitForServers.add(serverModelName); + } + } + context.completeStep(OperationContext.RollbackHandler.NOOP_ROLLBACK_HANDLER); + } + }, Stage.RUNTIME); + context.stepCompleted(); + } + } } diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/DomainModelControllerService.java b/host-controller/src/main/java/org/jboss/as/host/controller/DomainModelControllerService.java index e061330e107..a8e4d838ba3 100644 --- a/host-controller/src/main/java/org/jboss/as/host/controller/DomainModelControllerService.java +++ b/host-controller/src/main/java/org/jboss/as/host/controller/DomainModelControllerService.java @@ -1009,6 +1009,21 @@ public void killServer(String serverName) { public void awaitServersState(Collection serverNames, boolean started) { getServerInventory().awaitServersState(serverNames, started); } + + @Override + public void suspendServer(String serverName) { + getServerInventory().suspendServer(serverName); + } + + @Override + public void resumeServer(String serverName) { + getServerInventory().resumeServer(serverName); + } + + @Override + public boolean awaitServerSuspend(Set waitForServers, int timeout) { + return getServerInventory().awaitServerSuspend(waitForServers, timeout); + } } private static S service(final Class service) { diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/ManagedServer.java b/host-controller/src/main/java/org/jboss/as/host/controller/ManagedServer.java index c5e3263b161..417f38593ad 100644 --- a/host-controller/src/main/java/org/jboss/as/host/controller/ManagedServer.java +++ b/host-controller/src/main/java/org/jboss/as/host/controller/ManagedServer.java @@ -26,6 +26,7 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RUNNING_SERVER; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.TIMEOUT; import static org.jboss.as.host.controller.logging.HostControllerLogger.ROOT_LOGGER; import java.io.IOException; @@ -159,7 +160,7 @@ byte[] getAuthKey() { * * @return the server name */ - public String getServerName() { + String getServerName() { return serverName; } @@ -168,7 +169,7 @@ public String getServerName() { * * @return the proxy controller */ - public TransformingProxyController getProxyController() { + TransformingProxyController getProxyController() { return proxyController; } @@ -177,7 +178,7 @@ public TransformingProxyController getProxyController() { * * @return the server status */ - public ServerStatus getState() { + ServerStatus getState() { final InternalState requiredState = this.requiredState; final InternalState state = internalState; if(requiredState == InternalState.FAILED) { @@ -198,14 +199,14 @@ public ServerStatus getState() { } } - protected boolean isRequiresReload() { + boolean isRequiresReload() { return requiresReload; } /** * Require a reload on the the next reconnect. */ - protected void requireReload() { + void requireReload() { requiresReload = true; } @@ -215,7 +216,7 @@ protected void requireReload() { * @param permit the controller permit * @return whether the state was changed successfully or not */ - protected synchronized boolean reload(int permit) { + synchronized boolean reload(int permit) { return internalSetState(new ReloadTask(permit), InternalState.SERVER_STARTED, InternalState.RELOADING); } @@ -224,7 +225,7 @@ protected synchronized boolean reload(int permit) { * * @param factory the boot command factory */ - protected synchronized void start(final ManagedServerBootCmdFactory factory) { + synchronized void start(final ManagedServerBootCmdFactory factory) { final InternalState required = this.requiredState; // Ignore if the server is already started if(required == InternalState.SERVER_STARTED) { @@ -248,17 +249,16 @@ protected synchronized void start(final ManagedServerBootCmdFactory factory) { /** * Stop a managed server. */ - protected synchronized void stop() { + synchronized void stop(int operationID, int timeout) { final InternalState required = this.requiredState; if(required != InternalState.STOPPED) { this.requiredState = InternalState.STOPPED; ROOT_LOGGER.stoppingServer(serverName); - // Transition, but don't wait for async notifications to complete - transition(false); + internalSetState(new ServerStopTask(operationID, timeout), internalState, InternalState.PROCESS_STOPPING); } } - protected synchronized void destroy() { + synchronized void destroy(int permit) { final InternalState required = this.requiredState; if(required == InternalState.STOPPED) { if(internalState != InternalState.STOPPED) { @@ -269,11 +269,11 @@ protected synchronized void destroy() { } } } else { - stop(); + stop(permit, 0); } } - protected synchronized void kill() { + synchronized void kill(int permit) { final InternalState required = this.requiredState; if(required == InternalState.STOPPED) { if(internalState != InternalState.STOPPED) { @@ -284,14 +284,14 @@ protected synchronized void kill() { } } } else { - stop(); + stop(permit, 0); } } /** * Try to reconnect to a started server. */ - protected synchronized void reconnectServerProcess(final ManagedServerBootCmdFactory factory) { + synchronized void reconnectServerProcess(final ManagedServerBootCmdFactory factory) { if(this.requiredState != InternalState.SERVER_STARTED) { this.bootConfiguration = factory; this.requiredState = InternalState.SERVER_STARTED; @@ -303,7 +303,7 @@ protected synchronized void reconnectServerProcess(final ManagedServerBootCmdFac /** * On host controller reload, remove a not running server registered in the process controller declared as down. */ - protected synchronized void removeServerProcess() { + synchronized void removeServerProcess() { this.requiredState = InternalState.STOPPED; internalSetState(new ProcessRemoveTask(), InternalState.STOPPED, InternalState.PROCESS_REMOVING); } @@ -311,7 +311,7 @@ protected synchronized void removeServerProcess() { /** * On host controller reload, remove a not running server registered in the process controller declared as stopping. */ - protected synchronized void setServerProcessStopping() { + synchronized void setServerProcessStopping() { this.requiredState = InternalState.STOPPED; internalSetState(null, InternalState.STOPPED, InternalState.PROCESS_STOPPING); } @@ -322,7 +322,7 @@ protected synchronized void setServerProcessStopping() { * @param expected the expected state * @return {@code true} if the state was reached, {@code false} otherwise */ - protected boolean awaitState(final InternalState expected) { + boolean awaitState(final InternalState expected) { synchronized (this) { final InternalState initialRequired = this.requiredState; for(;;) { @@ -351,18 +351,18 @@ protected boolean awaitState(final InternalState expected) { /** * Notification that the process was added */ - protected void processAdded() { + void processAdded() { finishTransition(InternalState.PROCESS_ADDING, InternalState.PROCESS_ADDED); } /** * Notification that the process was started. */ - protected void processStarted() { + void processStarted() { finishTransition(InternalState.PROCESS_STARTING, InternalState.PROCESS_STARTED); } - protected synchronized TransactionalProtocolClient channelRegistered(final ManagementChannelHandler channelAssociation) { + synchronized TransactionalProtocolClient channelRegistered(final ManagementChannelHandler channelAssociation) { final InternalState current = this.internalState; // Create the remote controller client channelAssociation.getAttachments().attach(TransactionalProtocolClient.SEND_SUBJECT, Boolean.TRUE); @@ -392,11 +392,11 @@ public boolean execute(final ManagedServer server) throws Exception { return remoteClient; } - protected synchronized void serverStarted(final TransitionTask task) { + synchronized void serverStarted(final TransitionTask task) { internalSetState(task, InternalState.SERVER_STARTING, InternalState.SERVER_STARTED); } - protected synchronized void serverStartFailed() { + synchronized void serverStartFailed() { internalSetState(null, InternalState.SERVER_STARTING, InternalState.FAILED); } @@ -407,7 +407,7 @@ protected synchronized void serverStartFailed() { * @param shuttingDown whether the server inventory is shutting down * @return whether the registration can be removed from the domain-controller */ - protected synchronized boolean callbackUnregistered(final TransactionalProtocolClient old, final boolean shuttingDown) { + synchronized boolean callbackUnregistered(final TransactionalProtocolClient old, final boolean shuttingDown) { // Disconnect the remote connection protocolClient.disconnected(old); @@ -439,7 +439,7 @@ protected synchronized boolean callbackUnregistered(final TransactionalProtocolC /** * Notification that the server process finished. */ - protected synchronized void processFinished() { + synchronized void processFinished() { final InternalState required = this.requiredState; final InternalState state = this.internalState; // If the server was not stopped @@ -454,7 +454,7 @@ protected synchronized void processFinished() { /** * Notification that the process got removed from the process controller. */ - protected void processRemoved() { + void processRemoved() { finishTransition(InternalState.PROCESS_REMOVING, InternalState.STOPPED); } @@ -550,7 +550,7 @@ private TransitionTask getTransitionTask(final InternalState next) { } case SERVER_STARTED: { return new ServerStartedTask(); } case PROCESS_STOPPING: { - return new ServerStopTask(); + return new ServerStopTask(-1, 0); } case PROCESS_REMOVING: { return new ProcessRemoveTask(); } default: { @@ -640,6 +640,66 @@ private static InternalState nextState(final InternalState state, final Internal return null; } + boolean suspend() { + + final ModelNode operation = new ModelNode(); + operation.get(OP).set("suspend"); + operation.get(OP_ADDR).setEmptyList(); + + try { + final TransactionalProtocolClient.PreparedOperation prepared = TransactionalProtocolHandlers.executeBlocking(operation, protocolClient); + if (prepared.isFailed()) { + return false; + } + prepared.commit(); + prepared.getFinalResult().get(); + } catch (Exception ignore) { + return false; + } + return true; + } + + + boolean resume() { + + final ModelNode operation = new ModelNode(); + operation.get(OP).set("resume"); + operation.get(OP_ADDR).setEmptyList(); + + try { + final TransactionalProtocolClient.PreparedOperation prepared = TransactionalProtocolHandlers.executeBlocking(operation, protocolClient); + if (prepared.isFailed()) { + return false; + } + prepared.commit(); + prepared.getFinalResult().get(); + } catch (Exception ignore) { + return false; + } + return true; + } + + void awaitSuspended(long timeout) { + + //we just re-suspend, but this time give a timeout + final ModelNode operation = new ModelNode(); + operation.get(OP).set("suspend"); + operation.get(OP_ADDR).setEmptyList(); + operation.get(TIMEOUT).set(timeout); + + try { + final TransactionalProtocolClient.PreparedOperation prepared = TransactionalProtocolHandlers.executeBlocking(operation, protocolClient); + if (prepared.isFailed()) { + return; + } + prepared.commit(); + prepared.getFinalResult().get(); + } catch (Exception ignore) { + return; + } + return; + } + static enum InternalState { STOPPED, @@ -654,6 +714,7 @@ static enum InternalState { PROCESS_STOPPING(true), PROCESS_STOPPED, PROCESS_REMOVING(true), + SUSPENDING(true), FAILED, ; @@ -669,7 +730,7 @@ static enum InternalState { this.async = async; } - public boolean isAsync() { + boolean isAsync() { return async; } } @@ -753,11 +814,46 @@ public boolean execute(ManagedServer server) throws Exception { private class ServerStopTask implements TransitionTask { + private final int permit; + private final int timeout; + + private ServerStopTask(int permit, int timeout) { + this.permit = permit; + this.timeout = timeout; + } + @Override public boolean execute(ManagedServer server) throws Exception { assert Thread.holdsLock(ManagedServer.this); // Call under lock // Stop process - processControllerClient.stopProcess(serverProcessName); + + try { + //graceful shutdown + //this just suspends the server, it does not actually shut it down + if (operationID != -1) { + + final ModelNode operation = new ModelNode(); + operation.get(OP).set("shutdown"); + operation.get(OP_ADDR).setEmptyList(); + operation.get("operation-id").set(permit); + operation.get("timeout").set(timeout); + + final TransactionalProtocolClient.PreparedOperation prepared = TransactionalProtocolHandlers.executeBlocking(operation, protocolClient); + if (prepared.isFailed()) { + return true; + } + //we stop the server via an operation + prepared.commit(); + prepared.getFinalResult().get(); + } + } catch (Exception ignore) { + } finally { + try { + processControllerClient.stopProcess(serverProcessName); + } catch (IOException ignore) { + + } + } return true; } } diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/ManagedServerProxy.java b/host-controller/src/main/java/org/jboss/as/host/controller/ManagedServerProxy.java index 7f1044c5c51..99f671e2674 100644 --- a/host-controller/src/main/java/org/jboss/as/host/controller/ManagedServerProxy.java +++ b/host-controller/src/main/java/org/jboss/as/host/controller/ManagedServerProxy.java @@ -30,7 +30,7 @@ import org.jboss.as.controller.client.OperationMessageHandler; import org.jboss.as.controller.remote.TransactionalProtocolClient; import org.jboss.as.controller.remote.TransactionalProtocolHandlers; -import org.jboss.as.protocol.logging.ProtocolLogger; +import org.jboss.as.host.controller.logging.HostControllerLogger; import org.jboss.as.server.operations.ServerProcessStateHandler; import org.jboss.dmr.ModelNode; import org.jboss.threads.AsyncFuture; @@ -94,7 +94,7 @@ public AsyncFuture execute(TransactionalOperationListener @Override public AsyncFuture execute(TransactionalOperationListener listener, T operation) throws IOException { - throw ProtocolLogger.ROOT_LOGGER.channelClosed(); + throw HostControllerLogger.ROOT_LOGGER.channelClosed(); } } diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/ServerInventory.java b/host-controller/src/main/java/org/jboss/as/host/controller/ServerInventory.java index 56330f7dacb..6b84243c6c5 100644 --- a/host-controller/src/main/java/org/jboss/as/host/controller/ServerInventory.java +++ b/host-controller/src/main/java/org/jboss/as/host/controller/ServerInventory.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.Map; +import java.util.Set; import javax.security.auth.callback.CallbackHandler; @@ -299,4 +300,26 @@ public interface ServerInventory { */ void awaitServersState(Collection serverNames, boolean started); + /** + * Suspends a server, allowing current requests to finish and blocking any new requests + * from starting. + * + * @param serverName The server name + */ + void suspendServer(String serverName); + + /** + * Resumes a server, allowing it to begin processing requests normally + * @param serverName The server name + */ + void resumeServer(String serverName); + + /** + * Waits for the given set of servers to suspend + * @param waitForServers The servers to wait for + * @param timeout The maximum amount of time to wait in milliseconds, with -1 meaning indefinitly + * @return true if all the servers suspended in time + */ + boolean awaitServerSuspend(Set waitForServers, int timeout); + } diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/ServerInventoryImpl.java b/host-controller/src/main/java/org/jboss/as/host/controller/ServerInventoryImpl.java index 6b4cc0ee756..6be99c61fa4 100644 --- a/host-controller/src/main/java/org/jboss/as/host/controller/ServerInventoryImpl.java +++ b/host-controller/src/main/java/org/jboss/as/host/controller/ServerInventoryImpl.java @@ -45,6 +45,7 @@ import java.util.Locale; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; @@ -238,7 +239,8 @@ public ServerStatus stopServer(final String serverName, final int gracefulTimeou if(server == null) { return ServerStatus.STOPPED; } - server.stop(); + Integer currentOperationID = CurrentOperationIdHolder.getCurrentOperationID(); + server.stop(currentOperationID == null ? -1 : currentOperationID, gracefulTimeout); if(blocking) { server.awaitState(ManagedServer.InternalState.STOPPED); } @@ -302,7 +304,7 @@ public void destroyServer(String serverName) { if(server == null) { return; } - server.destroy(); + server.destroy(CurrentOperationIdHolder.getCurrentOperationID()); } @Override @@ -311,7 +313,7 @@ public void killServer(String serverName) { if(server == null) { return; } - server.kill(); + server.kill(CurrentOperationIdHolder.getCurrentOperationID()); } @Override @@ -322,7 +324,8 @@ public void stopServers(final int gracefulTimeout) { @Override public void stopServers(final int gracefulTimeout, final boolean blockUntilStopped) { for(final ManagedServer server : servers.values()) { - server.stop(); + Integer currentOperationID = CurrentOperationIdHolder.getCurrentOperationID(); + server.stop(currentOperationID == null ? -1 : currentOperationID, gracefulTimeout); } if(blockUntilStopped) { synchronized (shutdownCondition) { @@ -368,6 +371,47 @@ public void awaitServersState(final Collection serverNames, final boolea } } + @Override + public void suspendServer(String serverName) { + final ManagedServer server = servers.get(serverName); + if(server == null) { + return; + } + server.suspend(); + } + + @Override + public void resumeServer(String serverName) { + final ManagedServer server = servers.get(serverName); + if(server == null) { + return; + } + server.resume(); + } + + @Override + public boolean awaitServerSuspend(Set waitForServers, int timeout) { + long end = System.currentTimeMillis() + timeout; + for (String serverName : waitForServers) { + + final ManagedServer server = servers.get(serverName); + if (server != null) { + if (timeout == -1) { + server.awaitSuspended(-1); + } else { + long time = end - System.currentTimeMillis(); + if (time > 0) { + server.awaitSuspended(time); + } else { + return false; + } + } + } + } + long time = end - System.currentTimeMillis(); + return time > 0; + } + void shutdown(final boolean shutdownServers, final int gracefulTimeout, final boolean blockUntilStopped) { final boolean shutdown = this.shutdown; this.shutdown = true; diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/logging/HostControllerLogger.java b/host-controller/src/main/java/org/jboss/as/host/controller/logging/HostControllerLogger.java index 48e514b3345..8a304652c23 100644 --- a/host-controller/src/main/java/org/jboss/as/host/controller/logging/HostControllerLogger.java +++ b/host-controller/src/main/java/org/jboss/as/host/controller/logging/HostControllerLogger.java @@ -1199,4 +1199,7 @@ void noDomainControllerConfigurationProvidedForAdminOnly(String policyAttribute, @LogMessage(level = Level.INFO) @Message(id = 152, value = "Server %s will be started with JVM launch command prefix '%s'") void serverLaunchCommandPrefix(String serverName, String launchCommandPrefix); + + @Message(id = 153, value = "Channel closed") + IOException channelClosed(); } diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerResumeHandler.java b/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerResumeHandler.java new file mode 100644 index 00000000000..132704822dc --- /dev/null +++ b/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerResumeHandler.java @@ -0,0 +1,87 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2013, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.jboss.as.host.controller.operations; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; + +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationDefinition; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.OperationStepHandler; +import org.jboss.as.controller.PathAddress; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.RunningMode; +import org.jboss.as.controller.SimpleOperationDefinitionBuilder; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.as.controller.registry.OperationEntry; +import org.jboss.as.host.controller.ServerInventory; +import org.jboss.as.host.controller.descriptions.HostResolver; +import org.jboss.as.host.controller.logging.HostControllerLogger; +import org.jboss.dmr.ModelNode; + +/** + * @author Stuart Douglas + */ +public class ServerResumeHandler implements OperationStepHandler { + + public static final String OPERATION_NAME = ModelDescriptionConstants.RESUME; + public static final OperationDefinition DEFINITION = getOperationDefinition(); + + private final ServerInventory serverInventory; + + public ServerResumeHandler(ServerInventory serverInventory) { + this.serverInventory = serverInventory; + } + + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + + if (context.getRunningMode() == RunningMode.ADMIN_ONLY) { + throw new OperationFailedException(new ModelNode(HostControllerLogger.ROOT_LOGGER.cannotStartServersInvalidMode(context.getRunningMode()))); + } + + final PathAddress address = PathAddress.pathAddress(operation.require(OP_ADDR)); + final PathElement element = address.getLastElement(); + final String serverName = element.getValue(); + + context.addStep(new OperationStepHandler() { + @Override + public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException { + // WFLY-2189 trigger a write-runtime authz check + context.getServiceRegistry(true); + + serverInventory.resumeServer(serverName); + context.stepCompleted(); + } + }, OperationContext.Stage.RUNTIME); + + context.stepCompleted(); + } + + static OperationDefinition getOperationDefinition() { + return new SimpleOperationDefinitionBuilder(OPERATION_NAME, HostResolver.getResolver("host.server")) + .setRuntimeOnly() + .withFlag(OperationEntry.Flag.HOST_CONTROLLER_ONLY) + .build(); + } +} diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerStartHandler.java b/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerStartHandler.java index 82c8012ff06..213015bf229 100644 --- a/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerStartHandler.java +++ b/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerStartHandler.java @@ -64,13 +64,16 @@ public class ServerStartHandler implements OperationStepHandler { private final ServerInventory serverInventory; - static OperationDefinition getOperationDefinition(String name) { - return new SimpleOperationDefinitionBuilder(name, HostResolver.getResolver("host.server")) - .setParameters(SERVER, BLOCKING) - .setReplyType(ModelType.STRING) - .setRuntimeOnly() - .withFlag(OperationEntry.Flag.HOST_CONTROLLER_ONLY) - .build(); + static OperationDefinition getOperationDefinition(String name, AttributeDefinition... additional) { + SimpleOperationDefinitionBuilder builder = new SimpleOperationDefinitionBuilder(name, HostResolver.getResolver("host.server")) + .setParameters(SERVER, BLOCKING) + .setReplyType(ModelType.STRING) + .setRuntimeOnly() + .withFlag(OperationEntry.Flag.HOST_CONTROLLER_ONLY); + for (AttributeDefinition param : additional) { + builder.addParameter(param); + } + return builder.build(); } /** diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerStopHandler.java b/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerStopHandler.java index b882aac37ee..bdd415ba801 100644 --- a/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerStopHandler.java +++ b/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerStopHandler.java @@ -21,15 +21,21 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; +import java.util.EnumSet; import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationDefinition; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.OperationStepHandler; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.access.Action; import org.jboss.as.controller.client.helpers.domain.ServerStatus; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; import org.jboss.as.host.controller.ServerInventory; import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; /** * Stops a server. @@ -38,8 +44,14 @@ */ public class ServerStopHandler implements OperationStepHandler { + private static final SimpleAttributeDefinition TIMEOUT = new SimpleAttributeDefinitionBuilder(ModelDescriptionConstants.TIMEOUT, ModelType.INT) + .setDefaultValue(new ModelNode(0)) + .setAllowNull(true) + .build(); + public static final String OPERATION_NAME = "stop"; - public static final OperationDefinition DEFINITION = ServerStartHandler.getOperationDefinition(OPERATION_NAME); + public static final OperationDefinition DEFINITION = ServerStartHandler.getOperationDefinition(OPERATION_NAME, TIMEOUT); + private final ServerInventory serverInventory; @@ -60,13 +72,20 @@ public void execute(OperationContext context, ModelNode operation) throws Operat final PathElement element = address.getLastElement(); final String serverName = element.getValue(); final boolean blocking = operation.get("blocking").asBoolean(false); + final int timeout = TIMEOUT.resolveModelAttribute(context, operation).asInt(); context.addStep(new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { - // WFLY-2189 trigger a write-runtime authz check - context.getServiceRegistry(true); + // WFLY-2741 -- DO NOT call context.getServiceRegistry(true) as that will trigger blocking for + // service container stability and one use case for this op is to recover from a + // messed up service container from a previous op. Instead just ask for authorization. + // Note that we already have the exclusive lock, so we are just skipping waiting for stability. + // If another op that is a step in a composite step with this op needs to modify the container + // it will have to wait for container stability, so skipping this only matters for the case + // where this step is the only runtime change. + context.authorize(operation, EnumSet.of(Action.ActionEffect.WRITE_RUNTIME)); - final ServerStatus status = serverInventory.stopServer(serverName, -1, blocking); + final ServerStatus status = serverInventory.stopServer(serverName, timeout, blocking); context.getResult().set(status.toString()); context.completeStep(OperationContext.RollbackHandler.NOOP_ROLLBACK_HANDLER); } diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerSuspendHandler.java b/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerSuspendHandler.java new file mode 100644 index 00000000000..de02cdc7c77 --- /dev/null +++ b/host-controller/src/main/java/org/jboss/as/host/controller/operations/ServerSuspendHandler.java @@ -0,0 +1,102 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2013, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.jboss.as.host.controller.operations; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; + +import java.util.Collections; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationDefinition; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.OperationStepHandler; +import org.jboss.as.controller.PathAddress; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.RunningMode; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleOperationDefinitionBuilder; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.as.controller.registry.OperationEntry; +import org.jboss.as.host.controller.ServerInventory; +import org.jboss.as.host.controller.descriptions.HostResolver; +import org.jboss.as.host.controller.logging.HostControllerLogger; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; + +/** + * @author Stuart Douglas + */ +public class ServerSuspendHandler implements OperationStepHandler { + + public static final String OPERATION_NAME = ModelDescriptionConstants.SUSPEND; + private static final AttributeDefinition TIMEOUT = new SimpleAttributeDefinitionBuilder(ModelDescriptionConstants.TIMEOUT, ModelType.INT, true) + .setDefaultValue(new ModelNode(0)) + .build(); + + public static final OperationDefinition DEFINITION = getOperationDefinition(); + + private final ServerInventory serverInventory; + + + public ServerSuspendHandler(ServerInventory serverInventory) { + this.serverInventory = serverInventory; + } + + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + + if (context.getRunningMode() == RunningMode.ADMIN_ONLY) { + throw new OperationFailedException(new ModelNode(HostControllerLogger.ROOT_LOGGER.cannotStartServersInvalidMode(context.getRunningMode()))); + } + + final PathAddress address = PathAddress.pathAddress(operation.require(OP_ADDR)); + final PathElement element = address.getLastElement(); + final String serverName = element.getValue(); + final int timeout = TIMEOUT.resolveModelAttribute(context, operation).asInt(); //timeout in seconds + + context.addStep(new OperationStepHandler() { + @Override + public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException { + // WFLY-2189 trigger a write-runtime authz check + context.getServiceRegistry(true); + + serverInventory.suspendServer(serverName); + if(timeout!= 0) { + serverInventory.awaitServerSuspend(Collections.singleton(serverName), timeout > 0 ? timeout * 1000 : timeout); + } + context.stepCompleted(); + } + }, OperationContext.Stage.RUNTIME); + + context.stepCompleted(); + } + + static OperationDefinition getOperationDefinition() { + assert TIMEOUT != null; //this can happen if the order of the contents is wrong + return new SimpleOperationDefinitionBuilder(OPERATION_NAME, HostResolver.getResolver("host.server")) + .setParameters(TIMEOUT) + .setRuntimeOnly() + .withFlag(OperationEntry.Flag.HOST_CONTROLLER_ONLY) + .build(); + } +} diff --git a/host-controller/src/main/java/org/jboss/as/host/controller/resources/ServerConfigResourceDefinition.java b/host-controller/src/main/java/org/jboss/as/host/controller/resources/ServerConfigResourceDefinition.java index 23047e69d85..9ed1617c238 100644 --- a/host-controller/src/main/java/org/jboss/as/host/controller/resources/ServerConfigResourceDefinition.java +++ b/host-controller/src/main/java/org/jboss/as/host/controller/resources/ServerConfigResourceDefinition.java @@ -55,9 +55,11 @@ import org.jboss.as.host.controller.operations.ServerRemoveHandler; import org.jboss.as.host.controller.operations.ServerRestartHandler; import org.jboss.as.host.controller.operations.ServerRestartRequiredServerConfigWriteAttributeHandler; +import org.jboss.as.host.controller.operations.ServerResumeHandler; import org.jboss.as.host.controller.operations.ServerStartHandler; import org.jboss.as.host.controller.operations.ServerStatusHandler; import org.jboss.as.host.controller.operations.ServerStopHandler; +import org.jboss.as.host.controller.operations.ServerSuspendHandler; import org.jboss.as.server.controller.resources.SystemPropertyResourceDefinition; import org.jboss.as.server.controller.resources.SystemPropertyResourceDefinition.Location; import org.jboss.as.server.services.net.SpecifiedInterfaceAddHandler; @@ -191,6 +193,8 @@ public static void registerServerLifecycleOperations(final ManagementResourceReg resourceRegistration.registerOperationHandler(ServerStopHandler.DEFINITION, stopHandler); final ServerReloadHandler reloadHandler = new ServerReloadHandler(serverInventory); resourceRegistration.registerOperationHandler(ServerReloadHandler.DEFINITION, reloadHandler); + resourceRegistration.registerOperationHandler(ServerSuspendHandler.DEFINITION, new ServerSuspendHandler(serverInventory)); + resourceRegistration.registerOperationHandler(ServerResumeHandler.DEFINITION, new ServerResumeHandler(serverInventory)); ServerProcessHandlers.ServerDestroyHandler destroyHandler = new ServerProcessHandlers.ServerDestroyHandler(serverInventory); resourceRegistration.registerOperationHandler(ServerProcessHandlers.DESTROY_OPERATION, destroyHandler); ServerProcessHandlers.ServerKillHandler killHandler = new ServerProcessHandlers.ServerKillHandler(serverInventory); diff --git a/host-controller/src/main/resources/org/jboss/as/domain/controller/resources/LocalDescriptions.properties b/host-controller/src/main/resources/org/jboss/as/domain/controller/resources/LocalDescriptions.properties index a1d65389e9d..0c4a05b7d4f 100644 --- a/host-controller/src/main/resources/org/jboss/as/domain/controller/resources/LocalDescriptions.properties +++ b/host-controller/src/main/resources/org/jboss/as/domain/controller/resources/LocalDescriptions.properties @@ -30,12 +30,19 @@ domain.list-unclean-host-unregistrations=Returns a list of host names which did domain.reload-servers=Reloads all servers currently running in the domain. domain.reload-servers.blocking=Wait until the servers are fully started before returning from the operation. +domain.reload-servers.timeout=The graceful shutdown timeout. If this is zero then a graceful shutdown will not be attempted, if this is -1 then the server will wait for a graceful shutdown indefinitely. domain.restart-servers=Restarts all servers currently running in the domain. domain.restart-servers.blocking=Wait until the servers are fully started before returning from the operation. +domain.restart-servers.timeout=The graceful shutdown timeout. If this is zero then a graceful shutdown will not be attempted, if this is -1 then the server will wait for a graceful shutdown indefinitely. domain.start-servers=Starts all configured servers in the domain that are not currently running. domain.start-servers.blocking=Wait until the servers are fully started before returning from the operation. domain.stop-servers=Stops all servers currently running in the domain. domain.stop-servers.blocking=Wait until the servers are stopped before returning from the operation. +domain.stop-servers.timeout=The graceful shutdown timeout. If this is zero then a graceful shutdown will not be attempted, if this is -1 then the server will wait for a graceful shutdown indefinitely. +domain.resume-servers=Resumes processing on all servers in the domain +domain.suspend-servers=Suspends all servers in the domain, all current operations will finish, and no new operations will be allowed. +domain.suspend-servers.timeout=Timeout in seconds. If this is zero the operation will return immediately, -1 means that it will wait indefinitely. Note that the operation will not roll back if the timeout is exceeded, it just means that not all current requests completed in the specified timeout. + profile=A named set of subsystem configurations. profile.name=The name of the profile @@ -70,12 +77,19 @@ server-group.deployment.remove=Remove a deployment from the list of content avai server-group.reload-servers=Reloads all servers belonging to the server group currently running in the domain. server-group.reload-servers.blocking=Wait until the servers are fully started before returning from the operation. +server-group.reload-servers.timeout=The graceful shutdown timeout. If this is zero then a graceful shutdown will not be attempted, if this is -1 then the server will wait for a graceful shutdown indefinitely. server-group.restart-servers=Restarts all servers belonging to the server group currently running in the domain. server-group.restart-servers.blocking=Wait until the servers are fully started before returning from the operation. +server-group.restart-servers.timeout=The graceful shutdown timeout. If this is zero then a graceful shutdown will not be attempted, if this is -1 then the server will wait for a graceful shutdown indefinitely. server-group.start-servers=Starts all configured servers belonging to the server group in the domain that are not currently running. server-group.start-servers.blocking=Wait until the servers are fully started before returning from the operation. server-group.stop-servers=Stops all servers belonging to the server group currently running in the domain. server-group.stop-servers.blocking=Wait until the servers are fully stopped before returning from the operation. +server-group.stop-servers.timeout=The graceful shutdown timeout. If this is zero then a graceful shutdown will not be attempted, if this is -1 then the server will wait for a graceful shutdown indefinitely. + +server-group.suspend-servers=Suspends operations on all servers in the server group. All current operations will be allowed to finish, and new operations will be rejected. +server-group.suspend-servers.timeout=Timeout in seconds. If this is zero the operation will return immediately, -1 means that it will wait indefinitely. Note that the operation will not roll back if the timeout is exceeded, it just means that not all current requests completed in the specified timeout. +server-group.resume-servers=Resumes operations on all servers in the server group server-group.deployment-overlay=Links between a defined deployment overlay and deployments in this server group server-group.deployment-overlay.add=Adds a link to a deployment overlay diff --git a/host-controller/src/main/resources/org/jboss/as/host/controller/descriptions/LocalDescriptions.properties b/host-controller/src/main/resources/org/jboss/as/host/controller/descriptions/LocalDescriptions.properties index 6bdb9873116..860b43f5049 100644 --- a/host-controller/src/main/resources/org/jboss/as/host/controller/descriptions/LocalDescriptions.properties +++ b/host-controller/src/main/resources/org/jboss/as/host/controller/descriptions/LocalDescriptions.properties @@ -72,8 +72,12 @@ host.server.stop.server=The name of the server. host.server.stop.server.deprecated=This parameter is ignored, instead the name comes from the value of the last address element host.server.stop.blocking=Whether the operation should block and wait until the server is stopped. host.server.stop.reply=The status of the server following execution of this operation. +host.server.stop.timeout=Timeout in seconds. If this is set a graceful shutdown will be attempted. If this is zero (the default) then the server will shutdown immediately. A value larger than zero means the server will wait up to this many seconds for all active requests to finish. A value smaller than zero means that the server will wait indefinitely for all active requests to finish. host.server.destroy=Destroy the server process. In case the server is not in the stopping state, it will attempt to stop the server first. host.server.kill=Kill the server process. In case the server is not in the stopping state, it will attempt to stop the server first. This operation may not work on all platforms and will try to destroy the process if not available. +host.server.suspend=Suspends the server, all current operations will finish, and no new operations will be allowed. +host.server.suspend.timeout=Timeout in seconds. If this is zero the operation will return immediately, -1 means that it will wait indefinitely. Note that the operation will not roll back if the timeout is exceeded, it just means that not all current requests completed in the specified timeout. +host.server.resume=Resumes operations on this server. host.master=Whether this host is master host for the domain; i.e. whether this process is acting as the Domain Controller. host.resolve-expression-on-domain=Operation that accepts an expression as input (or a string that can be parsed into an expression) and resolves it against the local system properties and environment variables on all servers managed by this host controller. host.resolve-expression-on-domain.expression=The expression to resolve. diff --git a/server/src/main/java/org/jboss/as/server/DomainServerMain.java b/server/src/main/java/org/jboss/as/server/DomainServerMain.java index 971d73fa4ad..45512512327 100644 --- a/server/src/main/java/org/jboss/as/server/DomainServerMain.java +++ b/server/src/main/java/org/jboss/as/server/DomainServerMain.java @@ -125,30 +125,40 @@ public void activate(final ServiceActivatorContext serviceActivatorContext) { throw new IllegalStateException(); // not reached } finally { } - for (;;) try { - final String hostName = StreamUtils.readUTFZBytes(initialInput); - final int port = StreamUtils.readInt(initialInput); - final boolean managementSubsystemEndpoint = StreamUtils.readBoolean(initialInput); - final byte[] asAuthKey = new byte[16]; - StreamUtils.readFully(initialInput, asAuthKey); - - // Get the host-controller server client - final ServiceContainer container = containerFuture.get(); - final HostControllerClient client = getRequiredService(container, HostControllerConnectionService.SERVICE_NAME, HostControllerClient.class); - // Reconnect to the host-controller - client.reconnect(hostName, port, asAuthKey, managementSubsystemEndpoint); - - } catch (InterruptedIOException e) { - Thread.interrupted(); - // ignore - } catch (EOFException e) { - // this means it's time to exit - break; + for (;;) { + try { + final String hostName = StreamUtils.readUTFZBytes(initialInput); + final int port = StreamUtils.readInt(initialInput); + final boolean managementSubsystemEndpoint = StreamUtils.readBoolean(initialInput); + final byte[] asAuthKey = new byte[16]; + StreamUtils.readFully(initialInput, asAuthKey); + + // Get the host-controller server client + final ServiceContainer container = containerFuture.get(); + final HostControllerClient client = getRequiredService(container, HostControllerConnectionService.SERVICE_NAME, HostControllerClient.class); + // Reconnect to the host-controller + client.reconnect(hostName, port, asAuthKey, managementSubsystemEndpoint); + + } catch (InterruptedIOException e) { + Thread.interrupted(); + // ignore + } catch (EOFException e) { + // this means it's time to exit + break; + } catch (Exception e) { + e.printStackTrace(); + break; + } + } + //we may be attempting a graceful shutdown, in which case we need to wait + final ServiceContainer container; + try { + container = containerFuture.get(); + final GracefulShutdownService client = getRequiredService(container, GracefulShutdownService.SERVICE_NAME, GracefulShutdownService.class); + client.awaitSuspend(); } catch (Exception e) { e.printStackTrace(); - break; } - // Once the input stream is cut off, shut down System.exit(ExitCodes.NORMAL); throw new IllegalStateException(); // not reached diff --git a/server/src/main/java/org/jboss/as/server/GracefulShutdownService.java b/server/src/main/java/org/jboss/as/server/GracefulShutdownService.java new file mode 100644 index 00000000000..2c2c0a07702 --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/GracefulShutdownService.java @@ -0,0 +1,102 @@ +package org.jboss.as.server; + +import org.jboss.as.server.logging.ServerLogger; +import org.jboss.as.server.shutdown.OperationListener; +import org.jboss.as.server.shutdown.SuspendController; +import org.jboss.msc.service.Service; +import org.jboss.msc.service.ServiceName; +import org.jboss.msc.service.StartContext; +import org.jboss.msc.service.StartException; +import org.jboss.msc.service.StopContext; +import org.jboss.msc.value.InjectedValue; + + +/** + * A service that allows the server to wait until graceful shutdown is complete. + * + * This is mainly used to perform graceful shutdown in domain mode, to delay the System.exit() call + * until the server has suspended. + * + * @author Stuart Douglas + */ +public class GracefulShutdownService implements Service { + + public static final ServiceName SERVICE_NAME = ServiceName.JBOSS.append("server", "graceful-shutdown-service"); + + private final InjectedValue suspendControllerInjectedValue = new InjectedValue<>(); + + private boolean suspend = false; + private boolean shuttingDown = false; + private final Object lock = new Object(); + + private final OperationListener listener = new OperationListener() { + @Override + public void suspendStarted() { + synchronized (lock) { + suspend = true; + } + } + + @Override + public void complete() { + synchronized (lock) { + suspend = false; + lock.notifyAll(); + } + } + + @Override + public void cancelled() { + + synchronized (lock) { + suspend = false; + lock.notifyAll(); + } + } + + @Override + public void timeout() { + synchronized (lock) { + suspend = false; + lock.notifyAll(); + } + } + }; + + @Override + public void start(StartContext context) throws StartException { + suspendControllerInjectedValue.getValue().addListener(listener); + } + + @Override + public void stop(StopContext context) { + suspendControllerInjectedValue.getValue().removeListener(listener); + } + + public void startGracefulShutdown() { + synchronized (lock) { + shuttingDown = true; + } + } + + public void awaitSuspend() { + synchronized (lock) { + while (suspend && shuttingDown) { + try { + lock.wait(); + } catch (InterruptedException e) { + ServerLogger.AS_ROOT_LOGGER.debug("Exception waiting for graceful shutdown", e); + } + } + } + } + + public InjectedValue getSuspendControllerInjectedValue() { + return suspendControllerInjectedValue; + } + + @Override + public GracefulShutdownService getValue() throws IllegalStateException, IllegalArgumentException { + return this; + } +} diff --git a/server/src/main/java/org/jboss/as/server/ServerService.java b/server/src/main/java/org/jboss/as/server/ServerService.java index 8227bc3892a..78ff4df7da4 100644 --- a/server/src/main/java/org/jboss/as/server/ServerService.java +++ b/server/src/main/java/org/jboss/as/server/ServerService.java @@ -111,7 +111,9 @@ import org.jboss.as.server.moduleservice.ExtensionIndexService; import org.jboss.as.server.moduleservice.ExternalModuleService; import org.jboss.as.server.moduleservice.ServiceModuleLoader; +import org.jboss.as.server.requestcontroller.GlobalRequestController; import org.jboss.as.server.services.security.AbstractVaultReader; +import org.jboss.as.server.shutdown.SuspendController; import org.jboss.dmr.ModelNode; import org.jboss.msc.service.Service; import org.jboss.msc.service.ServiceBuilder; @@ -267,6 +269,18 @@ protected void boot(final BootContext context) throws ConfigurationPersistenceEx final DeploymentOverlayIndexService deploymentOverlayIndexService = new DeploymentOverlayIndexService(); context.getServiceTarget().addService(DeploymentOverlayIndexService.SERVICE_NAME, deploymentOverlayIndexService).install(); + context.getServiceTarget().addService(SuspendController.SERVICE_NAME, new SuspendController()).install(); + + GracefulShutdownService gracefulShutdownService = new GracefulShutdownService(); + context.getServiceTarget().addService(GracefulShutdownService.SERVICE_NAME, gracefulShutdownService) + .addDependency(SuspendController.SERVICE_NAME, SuspendController.class, gracefulShutdownService.getSuspendControllerInjectedValue()) + .install(); + + GlobalRequestController globalRequestController = new GlobalRequestController(); + context.getServiceTarget().addService(GlobalRequestController.SERVICE_NAME, globalRequestController) + .addDependency(SuspendController.SERVICE_NAME, SuspendController.class, globalRequestController.getShutdownControllerInjectedValue()) + .install(); + // Activate module loader DeployerChainAddHandler.addDeploymentProcessor(SERVER_NAME, Phase.STRUCTURE, Phase.STRUCTURE_SERVICE_MODULE_LOADER, new DeploymentUnitProcessor() { @Override diff --git a/server/src/main/java/org/jboss/as/server/ServerStartTask.java b/server/src/main/java/org/jboss/as/server/ServerStartTask.java index 16588da3e9d..995312b97be 100644 --- a/server/src/main/java/org/jboss/as/server/ServerStartTask.java +++ b/server/src/main/java/org/jboss/as/server/ServerStartTask.java @@ -147,6 +147,8 @@ public void activate(ServiceActivatorContext serviceActivatorContext) throws Ser .addDependency(Services.JBOSS_SERVER_EXECUTOR, Executor.class, service.getExecutorInjector()) .setInitialMode(ServiceController.Mode.ACTIVE) .install(); + + } }; services.add(activator); diff --git a/server/src/main/java/org/jboss/as/server/controller/resources/ServerRootResourceDefinition.java b/server/src/main/java/org/jboss/as/server/controller/resources/ServerRootResourceDefinition.java index 8fafa96298f..35281c89eee 100644 --- a/server/src/main/java/org/jboss/as/server/controller/resources/ServerRootResourceDefinition.java +++ b/server/src/main/java/org/jboss/as/server/controller/resources/ServerRootResourceDefinition.java @@ -96,9 +96,12 @@ import org.jboss.as.server.operations.RootResourceHack; import org.jboss.as.server.operations.RunningModeReadHandler; import org.jboss.as.server.operations.ServerDomainProcessReloadHandler; +import org.jboss.as.server.operations.ServerDomainProcessShutdownHandler; import org.jboss.as.server.operations.ServerProcessReloadHandler; import org.jboss.as.server.operations.ServerProcessStateHandler; +import org.jboss.as.server.operations.ServerResumeHandler; import org.jboss.as.server.operations.ServerShutdownHandler; +import org.jboss.as.server.operations.ServerSuspendHandler; import org.jboss.as.server.operations.ServerVersionOperations.DefaultEmptyListAttributeHandler; import org.jboss.as.server.operations.SetServerGroupHostHandler; import org.jboss.as.server.services.net.BindingGroupAddHandler; @@ -284,8 +287,17 @@ public void registerOperations(ManagementResourceRegistration resourceRegistrati // TODO enable the descriptions so that they show up in the resource description final ServerDomainProcessReloadHandler reloadHandler = new ServerDomainProcessReloadHandler(Services.JBOSS_AS, runningModeControl, processState, operationIDUpdater); resourceRegistration.registerOperationHandler(ServerDomainProcessReloadHandler.DOMAIN_DEFINITION, reloadHandler, false); - resourceRegistration.registerOperationHandler(SetServerGroupHostHandler.DEFINITION, SetServerGroupHostHandler.INSTANCE); + + final ServerSuspendHandler suspendHandler = new ServerSuspendHandler(); + resourceRegistration.registerOperationHandler(ServerSuspendHandler.DOMAIN_DEFINITION, suspendHandler, false); + + final ServerResumeHandler resumeHandler = new ServerResumeHandler(); + resourceRegistration.registerOperationHandler(ServerResumeHandler.DOMAIN_DEFINITION, resumeHandler, false); + + resourceRegistration.registerOperationHandler(ServerDomainProcessShutdownHandler.DOMAIN_DEFINITION, new ServerDomainProcessShutdownHandler(operationIDUpdater), false); + + // // Trick the resource-description for domain servers to be included in the server-resource // resourceRegistration.registerOperationHandler(getOperationDefinition("start"), NOOP); // resourceRegistration.registerOperationHandler(getOperationDefinition("stop"), NOOP); @@ -295,6 +307,9 @@ public void registerOperations(ManagementResourceRegistration resourceRegistrati } else { final ServerProcessReloadHandler reloadHandler = new ServerProcessReloadHandler(Services.JBOSS_AS, runningModeControl, processState); resourceRegistration.registerOperationHandler(ServerProcessReloadHandler.DEFINITION, reloadHandler, false); + + resourceRegistration.registerOperationHandler(ServerSuspendHandler.DEFINITION, ServerSuspendHandler.INSTANCE); + resourceRegistration.registerOperationHandler(ServerResumeHandler.DEFINITION, ServerResumeHandler.INSTANCE); } // Runtime operations @@ -303,6 +318,7 @@ public void registerOperations(ManagementResourceRegistration resourceRegistrati if (serverEnvironment.getLaunchType() == ServerEnvironment.LaunchType.STANDALONE) { ServerShutdownHandler serverShutdownHandler = new ServerShutdownHandler(processState); resourceRegistration.registerOperationHandler(ServerShutdownHandler.DEFINITION, serverShutdownHandler); + } resourceRegistration.registerSubModel(ServerEnvironmentResourceDescription.of(serverEnvironment)); } diff --git a/server/src/main/java/org/jboss/as/server/deployment/DeploymentHandlerUtil.java b/server/src/main/java/org/jboss/as/server/deployment/DeploymentHandlerUtil.java index 041b5792f61..e1d87373f8f 100644 --- a/server/src/main/java/org/jboss/as/server/deployment/DeploymentHandlerUtil.java +++ b/server/src/main/java/org/jboss/as/server/deployment/DeploymentHandlerUtil.java @@ -23,6 +23,8 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAME; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RUNTIME_NAME; import static org.jboss.as.server.deployment.DeploymentHandlerUtils.getContents; + +import org.jboss.as.server.requestcontroller.GlobalRequestController; import org.jboss.as.server.services.security.AbstractVaultReader; import static org.jboss.msc.service.ServiceController.Mode.REMOVE; @@ -307,6 +309,12 @@ public static void undeploy(final OperationContext context, final String managem context.addStep(new OperationStepHandler() { public void execute(OperationContext context, ModelNode operation) { + final ServiceRegistry registry = context.getServiceRegistry(true); + + //we immediatly pause all requests to the deployment + ServiceController shutdownController = (ServiceController) registry.getRequiredService(GlobalRequestController.SERVICE_NAME); + shutdownController.getValue().pauseDeployment(deploymentUnitName, null); + final ServiceName deploymentUnitServiceName = Services.deploymentUnitName(deploymentUnitName); context.removeService(deploymentUnitServiceName); @@ -316,13 +324,15 @@ public void execute(OperationContext context, ModelNode operation) { @Override public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) { if(resultAction == OperationContext.ResultAction.ROLLBACK) { + ServiceController shutdownController = (ServiceController) registry.getRequiredService(GlobalRequestController.SERVICE_NAME); + final ModelNode model = context.readResource(PathAddress.EMPTY_ADDRESS).getModel(); final String name = model.require(NAME).asString(); final String runtimeName = model.require(RUNTIME_NAME).asString(); final DeploymentHandlerUtil.ContentItem[] contents = getContents(model.require(CONTENT)); final ServiceVerificationHandler verificationHandler = new ServiceVerificationHandler(); doDeploy(context, runtimeName, name, verificationHandler, deployment, registration, mutableRegistration, vaultReader, contents); - + shutdownController.getValue().resumeDeployment(deploymentUnitName); if (context.hasFailureDescription()) { ServerLogger.ROOT_LOGGER.undeploymentRolledBack(deploymentUnitName, getFormattedFailureDescription(context)); } else { diff --git a/server/src/main/java/org/jboss/as/server/deployment/Phase.java b/server/src/main/java/org/jboss/as/server/deployment/Phase.java index 010a7c71fbb..a65b8d7e86f 100644 --- a/server/src/main/java/org/jboss/as/server/deployment/Phase.java +++ b/server/src/main/java/org/jboss/as/server/deployment/Phase.java @@ -491,6 +491,7 @@ public AttachmentKey getPhaseKey() { public static final int POST_MODULE_RAR_SERVICES_DEPS = 0x3300; public static final int POST_MODULE_UNDERTOW_MODCLUSTER = 0x3400; public static final int POST_MODULE_TRANSACTIONS_EE_CONCURRENCY = 0x3500; + public static final int POST_MODULE_EE_COMPONENT_SUSPEND = 0x3600; // INSTALL public static final int INSTALL_SHARED_SESSION_MANAGER = 0x0100; diff --git a/server/src/main/java/org/jboss/as/server/logging/ServerLogger.java b/server/src/main/java/org/jboss/as/server/logging/ServerLogger.java index 64dd3d083ae..a1163b3db53 100644 --- a/server/src/main/java/org/jboss/as/server/logging/ServerLogger.java +++ b/server/src/main/java/org/jboss/as/server/logging/ServerLogger.java @@ -1041,4 +1041,15 @@ OperationFailedException illegalCombinationOfHttpManagementInterfaceConfiguratio @Message(id = 209, value = "When specifying a 'module' you also need to specify the 'code'") OperationFailedException vaultModuleWithNoCode(); + + @Message(id = 210, value = "Server is already paused") + IllegalStateException serverAlreadyPaused(); + + @LogMessage(level = INFO) + @Message(id = 211, value = "Suspending server") + void suspendingServer(); + + @LogMessage(level = INFO) + @Message(id = 212, value = "Resuming server") + void resumingServer(); } diff --git a/server/src/main/java/org/jboss/as/server/operations/ServerDomainProcessShutdownHandler.java b/server/src/main/java/org/jboss/as/server/operations/ServerDomainProcessShutdownHandler.java new file mode 100644 index 00000000000..59a11a01739 --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/operations/ServerDomainProcessShutdownHandler.java @@ -0,0 +1,116 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2011, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.jboss.as.server.operations; + + +import java.util.EnumSet; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.OperationStepHandler; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleOperationDefinition; +import org.jboss.as.controller.SimpleOperationDefinitionBuilder; +import org.jboss.as.controller.access.Action; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.as.controller.registry.OperationEntry; +import org.jboss.as.server.DomainServerCommunicationServices; +import org.jboss.as.server.GracefulShutdownService; +import org.jboss.as.server.controller.descriptions.ServerDescriptions; +import org.jboss.as.server.shutdown.SuspendController; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; +import org.jboss.msc.service.ServiceController; +import org.jboss.msc.service.ServiceRegistry; + +/** + * Handler that starts a graceful shutdown in domain mode. + * + * Note that this does not actually shut down the server, it merely starts the suspend process. The server is shut down + * by the process controller closing the STDIN stream, which then waits for this process to be complete. + * + * @author Stuart Douglas + */ +public class ServerDomainProcessShutdownHandler implements OperationStepHandler { + + protected static final SimpleAttributeDefinition TIMEOUT = new SimpleAttributeDefinitionBuilder(ModelDescriptionConstants.TIMEOUT, ModelType.INT) + .setDefaultValue(new ModelNode(0)) + .setAllowNull(true) + .build(); + + public static final SimpleOperationDefinition DOMAIN_DEFINITION = new SimpleOperationDefinitionBuilder(ModelDescriptionConstants.SHUTDOWN, ServerDescriptions.getResourceDescriptionResolver()) + .setParameters(TIMEOUT) + .setPrivateEntry() // For now + .withFlags(OperationEntry.Flag.HOST_CONTROLLER_ONLY, OperationEntry.Flag.RUNTIME_ONLY) + .build(); + + private final DomainServerCommunicationServices.OperationIDUpdater operationIDUpdater; + + public ServerDomainProcessShutdownHandler(DomainServerCommunicationServices.OperationIDUpdater operationIDUpdater) { + this.operationIDUpdater = operationIDUpdater; + } + + @Override + public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException { + context.acquireControllerLock(); + // Update the operation permit + final int permit = operation.require("operation-id").asInt(); + operationIDUpdater.updateOperationID(permit); + final int timeout = TIMEOUT.resolveModelAttribute(context, operation).asInt(); //in milliseconds, as this is what is passed in from the HC + // Acquire the controller lock to prevent new write ops and wait until current ones are done + context.acquireControllerLock(); + context.addStep(new OperationStepHandler() { + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + // WFLY-2741 -- DO NOT call context.getServiceRegistry(true) as that will trigger blocking for + // service container stability and one use case for this op is to recover from a + // messed up service container from a previous op. Instead just ask for authorization. + // Note that we already have the exclusive lock, so we are just skipping waiting for stability. + // If another op that is a step in a composite step with this op needs to modify the container + // it will have to wait for container stability, so skipping this only matters for the case + // where this step is the only runtime change. +// context.getServiceRegistry(true); + context.authorize(operation, EnumSet.of(Action.ActionEffect.WRITE_RUNTIME)); + context.completeStep(new OperationContext.ResultHandler() { + @Override + public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) { + if (resultAction == OperationContext.ResultAction.KEEP) { + //even if the timeout is zero we still pause the server + //to stop new requests being accepted as it is shutting down + final ServiceRegistry registry = context.getServiceRegistry(false); + + final ServiceController gracefulController = (ServiceController) registry.getRequiredService(GracefulShutdownService.SERVICE_NAME); + gracefulController.getValue().startGracefulShutdown(); + + final ServiceController suspendControllerServiceController = (ServiceController) registry.getRequiredService(SuspendController.SERVICE_NAME); + final SuspendController suspendController = suspendControllerServiceController.getValue(); + suspendController.suspend(timeout > 0 ? timeout * 1000 : timeout); + } + } + }); + } + }, OperationContext.Stage.RUNTIME); + + context.stepCompleted(); + } +} diff --git a/server/src/main/java/org/jboss/as/server/operations/ServerResumeHandler.java b/server/src/main/java/org/jboss/as/server/operations/ServerResumeHandler.java new file mode 100644 index 00000000000..5384e77e35b --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/operations/ServerResumeHandler.java @@ -0,0 +1,88 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2011, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.jboss.as.server.operations; + + +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationDefinition; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.OperationStepHandler; +import org.jboss.as.controller.SimpleOperationDefinition; +import org.jboss.as.controller.SimpleOperationDefinitionBuilder; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.as.controller.registry.OperationEntry; +import org.jboss.as.server.controller.descriptions.ServerDescriptions; +import org.jboss.as.server.shutdown.SuspendController; +import org.jboss.dmr.ModelNode; +import org.jboss.msc.service.ServiceController; +import org.jboss.msc.service.ServiceRegistry; + +/** + * Handler that suspends server operations + * + * @author Stuart Douglas + */ +public class ServerResumeHandler implements OperationStepHandler { + protected static final String OPERATION_NAME = ModelDescriptionConstants.RESUME; + + public static final ServerResumeHandler INSTANCE = new ServerResumeHandler(); + + public static final SimpleOperationDefinition DEFINITION = new SimpleOperationDefinitionBuilder(ModelDescriptionConstants.RESUME, ServerDescriptions.getResourceDescriptionResolver()) + .setRuntimeOnly() + .build(); + + public static final OperationDefinition DOMAIN_DEFINITION = new SimpleOperationDefinitionBuilder(OPERATION_NAME, ServerDescriptions.getResourceDescriptionResolver()) + .setPrivateEntry() // For now + .withFlags(OperationEntry.Flag.HOST_CONTROLLER_ONLY, OperationEntry.Flag.RUNTIME_ONLY) + .build(); + + /** + * {@inheritDoc} + */ + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + // Acquire the controller lock to prevent new write ops and wait until current ones are done + context.acquireControllerLock(); + context.addStep(new OperationStepHandler() { + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + // Even though we don't read from the service registry, we have a reference to a + // service-fronted 'processState' so tell the controller we are modifying a service + final ServiceRegistry registry = context.getServiceRegistry(true); + context.completeStep(new OperationContext.ResultHandler() { + @Override + public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) { + if(resultAction == OperationContext.ResultAction.KEEP) { + //even if the timeout is zero we still pause the server + //to stop new requests being accepted as it is shutting down + ServiceController shutdownController = (ServiceController) registry.getRequiredService(SuspendController.SERVICE_NAME); + shutdownController.getValue().resume(); + } + } + }); + } + }, OperationContext.Stage.RUNTIME); + + context.stepCompleted(); + } +} diff --git a/server/src/main/java/org/jboss/as/server/operations/ServerShutdownHandler.java b/server/src/main/java/org/jboss/as/server/operations/ServerShutdownHandler.java index d36f893e563..a4a706ff956 100644 --- a/server/src/main/java/org/jboss/as/server/operations/ServerShutdownHandler.java +++ b/server/src/main/java/org/jboss/as/server/operations/ServerShutdownHandler.java @@ -37,8 +37,14 @@ import org.jboss.as.controller.descriptions.ModelDescriptionConstants; import org.jboss.as.process.ExitCodes; import org.jboss.as.server.controller.descriptions.ServerDescriptions; +import org.jboss.as.server.shutdown.OperationListener; +import org.jboss.as.server.shutdown.SuspendController; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; +import org.jboss.msc.service.ServiceController; +import org.jboss.msc.service.ServiceRegistry; + +import java.util.concurrent.atomic.AtomicBoolean; /** * Handler that shuts down the standalone server. @@ -47,12 +53,17 @@ */ public class ServerShutdownHandler implements OperationStepHandler { - private static final SimpleAttributeDefinition RESTART = new SimpleAttributeDefinitionBuilder(ModelDescriptionConstants.RESTART, ModelType.BOOLEAN) + protected static final SimpleAttributeDefinition RESTART = new SimpleAttributeDefinitionBuilder(ModelDescriptionConstants.RESTART, ModelType.BOOLEAN) .setDefaultValue(new ModelNode(false)) .setAllowNull(true) .build(); - public static final SimpleOperationDefinition DEFINITION = new SimpleOperationDefinitionBuilder("shutdown", ServerDescriptions.getResourceDescriptionResolver()) - .setParameters(RESTART) + protected static final SimpleAttributeDefinition TIMEOUT = new SimpleAttributeDefinitionBuilder(ModelDescriptionConstants.TIMEOUT, ModelType.INT) + .setDefaultValue(new ModelNode(0)) + .setAllowNull(true) + .build(); + + public static final SimpleOperationDefinition DEFINITION = new SimpleOperationDefinitionBuilder(ModelDescriptionConstants.SHUTDOWN, ServerDescriptions.getResourceDescriptionResolver()) + .setParameters(RESTART, TIMEOUT) .setRuntimeOnly() .build(); @@ -69,6 +80,7 @@ public ServerShutdownHandler(ControlledProcessState processState) { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { final boolean restart = RESTART.resolveModelAttribute(context, operation).asBoolean(); + final int timeout = TIMEOUT.resolveModelAttribute(context, operation).asInt(); //in seconds, need to convert to ms // Acquire the controller lock to prevent new write ops and wait until current ones are done context.acquireControllerLock(); context.addStep(new OperationStepHandler() { @@ -87,16 +99,42 @@ public void execute(OperationContext context, ModelNode operation) throws Operat @Override public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) { if(resultAction == OperationContext.ResultAction.KEEP) { - processState.setStopping(); - final Thread thread = new Thread(new Runnable() { - public void run() { - System.exit(restart ? ExitCodes.RESTART_PROCESS_FROM_STARTUP_SCRIPT : ExitCodes.NORMAL); + //even if the timeout is zero we still pause the server + //to stop new requests being accepted as it is shutting down + final ShutdownAction shutdown = new ShutdownAction(restart); + final ServiceRegistry registry = context.getServiceRegistry(false); + final ServiceController suspendControllerServiceController = (ServiceController) registry.getRequiredService(SuspendController.SERVICE_NAME); + final SuspendController suspendController = suspendControllerServiceController.getValue(); + OperationListener listener = new OperationListener() { + @Override + public void suspendStarted() { + + } + + @Override + public void complete() { + suspendController.removeListener(this); + shutdown.shutdown(); + } + + @Override + public void cancelled() { + suspendController.removeListener(this); + shutdown.cancel(); + } + + @Override + public void timeout() { + suspendController.removeListener(this); + shutdown.shutdown(); } - }); - // The intention is that this shutdown is graceful, and so the client gets a reply. - // At the time of writing we did not yet have graceful shutdown. - thread.setName("Management Triggered Shutdown"); - thread.start(); + }; + suspendController.addListener(listener); + suspendController.suspend(timeout > 0 ? timeout * 1000 : timeout); + if(timeout == 0) { + shutdown.shutdown(); + } + } } }); @@ -105,4 +143,32 @@ public void run() { context.stepCompleted(); } + + private final class ShutdownAction extends AtomicBoolean { + + private final boolean restart; + + private ShutdownAction(boolean restart) { + this.restart = restart; + } + + void cancel() { + compareAndSet(false, true); + } + + void shutdown() { + if(compareAndSet(false, true)) { + processState.setStopping(); + final Thread thread = new Thread(new Runnable() { + public void run() { + System.exit(restart ? ExitCodes.RESTART_PROCESS_FROM_STARTUP_SCRIPT : ExitCodes.NORMAL); + } + }); + // The intention is that this shutdown is graceful, and so the client gets a reply. + // At the time of writing we did not yet have graceful shutdown. + thread.setName("Management Triggered Shutdown"); + thread.start(); + } + } + } } diff --git a/server/src/main/java/org/jboss/as/server/operations/ServerSuspendHandler.java b/server/src/main/java/org/jboss/as/server/operations/ServerSuspendHandler.java new file mode 100644 index 00000000000..c69052b58b7 --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/operations/ServerSuspendHandler.java @@ -0,0 +1,138 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2013, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.jboss.as.server.operations; + + +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.OperationStepHandler; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleOperationDefinition; +import org.jboss.as.controller.SimpleOperationDefinitionBuilder; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.as.controller.registry.OperationEntry; +import org.jboss.as.server.controller.descriptions.ServerDescriptions; +import org.jboss.as.server.shutdown.OperationListener; +import org.jboss.as.server.shutdown.SuspendController; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; +import org.jboss.msc.service.ServiceController; +import org.jboss.msc.service.ServiceRegistry; + +/** + * Handler that suspends server operations + * + * @author Stuart Douglas + */ +public class ServerSuspendHandler implements OperationStepHandler { + + protected static final String OPERATION_NAME = ModelDescriptionConstants.SUSPEND; + + public static final ServerSuspendHandler INSTANCE = new ServerSuspendHandler(); + + /** + * The timeout in seconds, the operation will block until either the timeout is reached or the server successfully suspends + * + * -1 : wait indefinitely + * 0 : return immediately (default) + * >0 : wait n seconds + */ + protected static final SimpleAttributeDefinition TIMEOUT = new SimpleAttributeDefinitionBuilder(ModelDescriptionConstants.TIMEOUT, ModelType.INT) + .setDefaultValue(new ModelNode(0)) + .setAllowNull(true) + .build(); + + public static final SimpleOperationDefinition DEFINITION = new SimpleOperationDefinitionBuilder(ModelDescriptionConstants.SUSPEND, ServerDescriptions.getResourceDescriptionResolver()) + .setParameters(TIMEOUT) + .setRuntimeOnly() + .build(); + + public static final SimpleOperationDefinition DOMAIN_DEFINITION = new SimpleOperationDefinitionBuilder(ModelDescriptionConstants.SUSPEND, ServerDescriptions.getResourceDescriptionResolver()) + .setParameters(TIMEOUT) + .setPrivateEntry() // For now + .withFlags(OperationEntry.Flag.HOST_CONTROLLER_ONLY, OperationEntry.Flag.RUNTIME_ONLY) + .build(); + /** + * {@inheritDoc} + */ + @Override + public void execute(final OperationContext context, ModelNode operation) throws OperationFailedException { + // Acquire the controller lock to prevent new write ops and wait until current ones are done + final int timeout = TIMEOUT.resolveModelAttribute(context, operation).asInt(); //in seconds, need to convert to ms + context.acquireControllerLock(); + + context.addStep(new OperationStepHandler() { + @Override + public void execute(final OperationContext context, ModelNode operation) throws OperationFailedException { + final ServiceRegistry registry = context.getServiceRegistry(false); + ServiceController suspendControllerServiceController = (ServiceController) registry.getRequiredService(SuspendController.SERVICE_NAME); + final SuspendController suspendController = suspendControllerServiceController.getValue(); + OperationListener operationListener = new OperationListener() { + @Override + public void suspendStarted() { + + } + + @Override + public void complete() { + suspendController.removeListener(this); + context.completeStep(new RollbackHandler(suspendController)); + } + + @Override + public void cancelled() { + suspendController.removeListener(this); + context.setRollbackOnly(); + context.completeStep(new RollbackHandler(suspendController)); + } + + @Override + public void timeout() { + suspendController.removeListener(this); + context.completeStep(new RollbackHandler(suspendController)); + } + }; + suspendController.addListener(operationListener); + suspendController.suspend(timeout > 0 ? timeout * 1000 : timeout); + } + }, OperationContext.Stage.RUNTIME); + context.completeStep(OperationContext.ResultHandler.NOOP_RESULT_HANDLER); + } + + private static final class RollbackHandler implements OperationContext.ResultHandler { + + private final SuspendController controller; + + private RollbackHandler(SuspendController controller) { + this.controller = controller; + } + + @Override + public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) { + if(resultAction == OperationContext.ResultAction.ROLLBACK) { + controller.resume(); + } + } + } +} diff --git a/server/src/main/java/org/jboss/as/server/requestcontroller/ControlPoint.java b/server/src/main/java/org/jboss/as/server/requestcontroller/ControlPoint.java new file mode 100644 index 00000000000..76de16938a4 --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/requestcontroller/ControlPoint.java @@ -0,0 +1,176 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.jboss.as.server.requestcontroller; + +import org.jboss.as.server.logging.ServerLogger; +import org.jboss.as.server.shutdown.ServerActivityListener; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +/** + * A representation of an entry point into the application server, represented as both a deployment + * name and an entry point name. + * + * This should only be created using the top level name, as it does not generally make sense to shut + * down individual parts of a deployment. + * + * Note that requests are tracked at two levels, both at the entry point level and the request controller level. + * This allows for individual deployments/interfaces to be gracefully suspended, and also allows for the global + * request controller to limit the total number of active requests. + * + * @author Stuart Douglas + */ +public class ControlPoint { + + private static final AtomicIntegerFieldUpdater activeRequestCountUpdater = AtomicIntegerFieldUpdater.newUpdater(ControlPoint.class, "activeRequestCount"); + private static final AtomicReferenceFieldUpdater listenerUpdater = AtomicReferenceFieldUpdater.newUpdater(ControlPoint.class, ServerActivityListener.class, "listener"); + + private final GlobalRequestController controller; + private final String deployment; + private final String entryPoint; + + /** + * The number of active requests that are using this entry point + */ + @SuppressWarnings("unused") + private volatile int activeRequestCount = 0; + + /** + * If this entry point is paused + */ + private volatile boolean paused = false; + + @SuppressWarnings("unused") + private volatile ServerActivityListener listener = null; + + /** + * The number of services that are using this entry point. + * This is a deployment time measurement, not a runtime one + */ + private int referenceCount = 0; + + ControlPoint(GlobalRequestController controller, String deployment, String entryPoint) { + this.controller = controller; + this.deployment = deployment; + this.entryPoint = entryPoint; + } + + public String getEntryPoint() { + return entryPoint; + } + + public String getDeployment() { + return deployment; + } + + /** + * Pause the current entry point, and invoke the provided listener when all current requests have finished. + * + * @param requestCountListener The listener to invoke + */ + public void pause(ServerActivityListener requestCountListener) { + if(paused) { + throw ServerLogger.ROOT_LOGGER.serverAlreadyPaused(); + } + this.paused = true; + listenerUpdater.set(this, requestCountListener); + if (activeRequestCountUpdater.get(this) == 0) { + if(listenerUpdater.compareAndSet(this, requestCountListener, null)) { + requestCountListener.requestsComplete(); + } + } + } + + /** + * Cancel the pause operation + */ + public void resume() { + this.paused = false; + ServerActivityListener listener = listenerUpdater.get(this); + if(listener != null) { + if(listenerUpdater.compareAndSet(this, listener, null)) { + listener.unPaused(); + } + } + } + + + /** + * All tasks entering the system via this entry point must call this method. If it returns REJECTED then the + * task cannot be run, and its failure should be signaled back to the originator. + * + * If it returns {@code RUN} then the task should proceed as normal, and the {@link #requestComplete()} method + * must be called once the task is complete, usually via a try/finally construct. + * + */ + public RunResult beginRequest() throws Exception { + if(paused) { + return RunResult.REJECTED; + } + activeRequestCountUpdater.incrementAndGet(this); + RunResult runResult = controller.beginRequest(); + if(runResult == RunResult.REJECTED) { + decreaseRequestCount(); + } + return runResult; + } + + /** + * Method that should be invoked once (and only once) to signify that a request has finished. + * + * This cannot be done automatically when the handleRequest method completes, as some + * + */ + public void requestComplete() { + decreaseRequestCount(); + controller.requestComplete(); + } + + private void decreaseRequestCount() { + int result = activeRequestCountUpdater.decrementAndGet(this); + if(paused && result == 0) { + ServerActivityListener listener = listenerUpdater.get(this); + if(listener != null) { + if(listenerUpdater.compareAndSet(this, listener, null)) { + listener.requestsComplete(); + } + } + } + } + + public boolean isPaused() { + return paused; + } + + public int getActiveRequestCount() { + return activeRequestCountUpdater.get(this); + } + + synchronized int increaseReferenceCount() { + return ++referenceCount; + } + + synchronized int decreaseReferenceCount() { + return --referenceCount; + } +} diff --git a/server/src/main/java/org/jboss/as/server/requestcontroller/CountingRequestCountListener.java b/server/src/main/java/org/jboss/as/server/requestcontroller/CountingRequestCountListener.java new file mode 100644 index 00000000000..752c3e4d445 --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/requestcontroller/CountingRequestCountListener.java @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.jboss.as.server.requestcontroller; + +import org.jboss.as.server.shutdown.ServerActivityListener; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * RequestCountListener that till n notification have been received before notifying + * its delegate. + * + * @author Stuart Douglas + */ +class CountingRequestCountListener implements ServerActivityListener { + + private final AtomicInteger count; + + private final ServerActivityListener delegate; + + private final AtomicBoolean canceled = new AtomicBoolean(false); + + CountingRequestCountListener(int count, ServerActivityListener delegate) { + this.count = new AtomicInteger(count); + this.delegate = delegate; + } + + @Override + public void requestsComplete() { + if (count.decrementAndGet() == 0) { + delegate.requestsComplete(); + } + } + + @Override + public void unPaused() { + if (canceled.compareAndSet(false, true)) { + delegate.unPaused(); + } + } +} diff --git a/server/src/main/java/org/jboss/as/server/requestcontroller/EntryPointService.java b/server/src/main/java/org/jboss/as/server/requestcontroller/EntryPointService.java new file mode 100644 index 00000000000..af3258a6878 --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/requestcontroller/EntryPointService.java @@ -0,0 +1,74 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.jboss.as.server.requestcontroller; + +import org.jboss.msc.service.Service; +import org.jboss.msc.service.ServiceName; +import org.jboss.msc.service.ServiceTarget; +import org.jboss.msc.service.StartContext; +import org.jboss.msc.service.StartException; +import org.jboss.msc.service.StopContext; +import org.jboss.msc.value.InjectedValue; + +/** + * @author Stuart Douglas + */ +public class EntryPointService implements Service{ + + private static final ServiceName SERVICE_NAME = GlobalRequestController.SERVICE_NAME.append("entry-point"); + private final String deployment; + private final String entryPoint; + private volatile ControlPoint value; + private final InjectedValue globalRequestControllerInjectedValue = new InjectedValue<>(); + + EntryPointService(String deployment, String entryPoint) { + this.deployment = deployment; + this.entryPoint = entryPoint; + } + + public static ServiceName serviceName(final String deployment, final String entryPoint) { + return SERVICE_NAME.append(deployment, entryPoint); + } + + public static void install(final ServiceTarget target, final String deployment, final String entryPoint) { + EntryPointService service = new EntryPointService(deployment, entryPoint); + target.addService(serviceName(deployment, entryPoint), service) + .addDependency(GlobalRequestController.SERVICE_NAME, GlobalRequestController.class, service.globalRequestControllerInjectedValue) + .install(); + } + + @Override + public void start(StartContext startContext) throws StartException { + value = globalRequestControllerInjectedValue.getValue().getEntryPoint(deployment, entryPoint); + } + + @Override + public void stop(StopContext stopContext) { + globalRequestControllerInjectedValue.getValue().removeEntryPoint(value); + value = null; + } + + @Override + public ControlPoint getValue() throws IllegalStateException, IllegalArgumentException { + return value; + } +} diff --git a/server/src/main/java/org/jboss/as/server/requestcontroller/GlobalRequestController.java b/server/src/main/java/org/jboss/as/server/requestcontroller/GlobalRequestController.java new file mode 100644 index 00000000000..56ce8fad060 --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/requestcontroller/GlobalRequestController.java @@ -0,0 +1,329 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.jboss.as.server.requestcontroller; + +import org.jboss.as.server.shutdown.ServerActivity; +import org.jboss.as.server.shutdown.ServerActivityListener; +import org.jboss.as.server.shutdown.SuspendController; +import org.jboss.msc.service.Service; +import org.jboss.msc.service.ServiceName; +import org.jboss.msc.service.StartContext; +import org.jboss.msc.service.StartException; +import org.jboss.msc.service.StopContext; +import org.jboss.msc.value.InjectedValue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +import static org.jboss.as.server.requestcontroller.RunResult.REJECTED; +import static org.jboss.as.server.requestcontroller.RunResult.RUN; + +/** + * A controller that manages the active requests that are running in the container. + *

+ * There are two main use cases for this: + *

+ * 1) Graceful shutdown - When the number of active request reaches zero then the container can be gracefully shut down + * 2) Request limiting - This allows the total number of requests that are active to be limited. + *

+ * + * @author Stuart Douglas + */ +public class GlobalRequestController implements Service, ServerActivity { + + public static final ServiceName SERVICE_NAME = ServiceName.JBOSS.append("server", "global-request-controller"); + + private static final AtomicIntegerFieldUpdater activeRequestCountUpdater = AtomicIntegerFieldUpdater.newUpdater(GlobalRequestController.class, "activeRequestCount"); + private static final AtomicReferenceFieldUpdater listenerUpdater = AtomicReferenceFieldUpdater.newUpdater(GlobalRequestController.class, ServerActivityListener.class, "listener"); + + private volatile int maxRequestCount = 0; + + private volatile int activeRequestCount = 0; + + private volatile boolean paused = false; + + private final Map entryPoints = new HashMap<>(); + + private final InjectedValue shutdownControllerInjectedValue = new InjectedValue<>(); + + @SuppressWarnings("unused") + private volatile ServerActivityListener listener = null; + + /** + * Pause the controller. All existing requests will have a chance to finish, and once all requests are + * finished the provided listener will be invoked. + *

+ * While the container is paused no new requests will be accepted. + * + * @param requestCountListener The listener that will be notified when all requests are done + */ + public synchronized void pause(ServerActivityListener requestCountListener) { + this.paused = true; + listenerUpdater.set(this, requestCountListener); + + if (activeRequestCountUpdater.get(this) == 0) { + if (listenerUpdater.compareAndSet(this, requestCountListener, null)) { + requestCountListener.requestsComplete(); + } + } + } + + /** + * Unpause the server, allowing it to resume normal operations + */ + @Override + public synchronized void resume() { + this.paused = false; + ServerActivityListener listener = listenerUpdater.get(this); + if (listener != null) { + if (listenerUpdater.compareAndSet(this, listener, null)) { + listener.unPaused(); + } + } + } + + /** + * Pauses a given deployment + * + * @param deployment The deployment to pause + * @param listener The listener that will be notified when the pause is complete + */ + public synchronized void pauseDeployment(final String deployment, ServerActivityListener listener) { + final List eps = new ArrayList(); + for (ControlPoint ep : entryPoints.values()) { + if (ep.getDeployment().equals(deployment)) { + eps.add(ep); + } + } + CountingRequestCountListener realListener = new CountingRequestCountListener(eps.size(), listener); + for (ControlPoint ep : eps) { + ep.pause(realListener); + } + } + + /** + * resumed a given deployment + * + * @param deployment The deployment to resume + */ + public synchronized void resumeDeployment(final String deployment) { + final List eps = new ArrayList(); + for (ControlPoint ep : entryPoints.values()) { + if (ep.getDeployment().equals(deployment)) { + ep.resume(); + } + } + } + + /** + * Pauses a given entry point. This can be used to stop all requests though a given mechanism, e.g. all web requests + * + * @param entryPoint The entry point + * @param listener The listener + */ + public synchronized void pauseEntryPoint(final String entryPoint, ServerActivityListener listener) { + final List eps = new ArrayList(); + for (ControlPoint ep : entryPoints.values()) { + if (ep.getEntryPoint().equals(entryPoint)) { + eps.add(ep); + } + } + CountingRequestCountListener realListener = new CountingRequestCountListener(eps.size(), listener); + for (ControlPoint ep : eps) { + ep.pause(realListener); + } + } + + /** + * Resumes a given entry point type; + * + * @param entryPoint The entry point + */ + public synchronized void resumeEntryPoint(final String entryPoint) { + final List eps = new ArrayList(); + for (ControlPoint ep : entryPoints.values()) { + if (ep.getEntryPoint().equals(entryPoint)) { + ep.resume(); + } + } + } + + public synchronized RequestControllerState getState() { + final List eps = new ArrayList<>(); + for (ControlPoint controlPoint : entryPoints.values()) { + eps.add(new RequestControllerState.EntryPointState(controlPoint.getDeployment(), controlPoint.getEntryPoint(), controlPoint.isPaused(), controlPoint.getActiveRequestCount())); + } + return new RequestControllerState(paused, activeRequestCount, maxRequestCount, eps); + } + + RunResult beginRequest() { + int maxRequests = maxRequestCount; + int active = activeRequestCountUpdater.get(this); + boolean success = false; + while ((maxRequests <= 0 || active < maxRequests) && !paused) { + if (activeRequestCountUpdater.compareAndSet(this, active, active + 1)) { + success = true; + break; + } + active = activeRequestCountUpdater.get(this); + } + if (success) { + //re-check the paused state + //this is necessary because there is a race between checking paused and updating active requests + //if this happens we just call requestComplete(), as the listener can only be invoked once it does not + //matter if it has already been invoked + if(paused) { + requestComplete(); + return REJECTED; + } + return RUN; + } else { + return REJECTED; + } + } + + + void requestComplete() { + int result = activeRequestCountUpdater.decrementAndGet(this); + if (paused) { + if (paused && result == 0) { + ServerActivityListener listener = listenerUpdater.get(this); + if (listener != null) { + if (listenerUpdater.compareAndSet(this, listener, null)) { + listener.requestsComplete(); + } + } + } + } + } + + /** + * Gets an entry point for the given deployment . If one does not exist it will be created. + * + * Entry points are reference counted. If this method is called n times then {@link #removeEntryPoint(ControlPoint)} + * must also be called n times to clean up the entry points. + * + * @param deploymentName The top level deployment name + * @param entryPointName The entry point name + * @return The entry point + */ + public synchronized ControlPoint getEntryPoint(final String deploymentName, final String entryPointName) { + EntryPointIdentifier id = new EntryPointIdentifier(deploymentName, entryPointName); + ControlPoint ep = entryPoints.get(id); + if (ep == null) { + ep = new ControlPoint(this, deploymentName, entryPointName); + entryPoints.put(id, ep); + } + ep.increaseReferenceCount(); + return ep; + } + + /** + * Removes the specified entry point + * + * @param controlPoint The entry point + */ + public synchronized void removeEntryPoint(ControlPoint controlPoint) { + if (controlPoint.decreaseReferenceCount() == 0) { + EntryPointIdentifier id = new EntryPointIdentifier(controlPoint.getDeployment(), controlPoint.getEntryPoint()); + entryPoints.remove(id); + } + } + + /** + * @return The maximum number of requests that can be active at a time + */ + public int getMaxRequestCount() { + return maxRequestCount; + } + + /** + * Sets the maximum number of requests that can be active at a time. + *

+ * If this is higher that the number of currently running requests the no new requests + * will be able to run until the number of active requests has dropped below this level. + * + * @param maxRequestCount The max request count + */ + public void setMaxRequestCount(int maxRequestCount) { + this.maxRequestCount = maxRequestCount; + } + + /** + * @return true If the server is currently pause + */ + public boolean isPaused() { + return paused; + } + + @Override + public void start(StartContext startContext) throws StartException { + shutdownControllerInjectedValue.getValue().registerActivity(this); + } + + @Override + public void stop(StopContext stopContext) { + shutdownControllerInjectedValue.getValue().unRegisterActivity(this); + } + + @Override + public GlobalRequestController getValue() throws IllegalStateException, IllegalArgumentException { + return this; + } + + public InjectedValue getShutdownControllerInjectedValue() { + return shutdownControllerInjectedValue; + } + + private static final class EntryPointIdentifier { + private final String deployment, name; + + private EntryPointIdentifier(String deployment, String name) { + this.deployment = deployment; + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + EntryPointIdentifier that = (EntryPointIdentifier) o; + + if (deployment != null ? !deployment.equals(that.deployment) : that.deployment != null) return false; + if (name != null ? !name.equals(that.name) : that.name != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = deployment != null ? deployment.hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } + } +} diff --git a/server/src/main/java/org/jboss/as/server/requestcontroller/RequestControllerState.java b/server/src/main/java/org/jboss/as/server/requestcontroller/RequestControllerState.java new file mode 100644 index 00000000000..6559be158e2 --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/requestcontroller/RequestControllerState.java @@ -0,0 +1,92 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.jboss.as.server.requestcontroller; + +import java.util.Collections; +import java.util.List; + +/** + * + * Class that can be used to report of the current system state. + * + * @author Stuart Douglas + */ +public class RequestControllerState { + + private final boolean paused; + private final int outstandingRequests; + private final int maxRequests; + private final List entryPoints; + + public RequestControllerState(boolean paused, int outstandingRequests, int maxRequests, List entryPoints) { + this.paused = paused; + this.outstandingRequests = outstandingRequests; + this.maxRequests = maxRequests; + this.entryPoints = entryPoints; + } + + public boolean isPaused() { + return paused; + } + + public int getOutstandingRequests() { + return outstandingRequests; + } + + public int getMaxRequests() { + return maxRequests; + } + + public List getEntryPoints() { + return Collections.unmodifiableList(entryPoints); + } + + public static class EntryPointState { + private final String deployment; + private final String endpoint; + private final boolean paused; + private final int outstandingRequests; + + public EntryPointState(String deployment, String endpoint, boolean paused, int outstandingRequests) { + this.deployment = deployment; + this.endpoint = endpoint; + this.paused = paused; + this.outstandingRequests = outstandingRequests; + } + + public String getDeployment() { + return deployment; + } + + public String getEndpoint() { + return endpoint; + } + + public boolean isPaused() { + return paused; + } + + public int isOutstandingRequests() { + return outstandingRequests; + } + } +} diff --git a/server/src/main/java/org/jboss/as/server/requestcontroller/RunResult.java b/server/src/main/java/org/jboss/as/server/requestcontroller/RunResult.java new file mode 100644 index 00000000000..55a715396af --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/requestcontroller/RunResult.java @@ -0,0 +1,32 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.jboss.as.server.requestcontroller; + +/** + * @author Stuart Douglas + */ +public enum RunResult { + + RUN, + REJECTED; +} diff --git a/server/src/main/java/org/jboss/as/server/shutdown/OperationListener.java b/server/src/main/java/org/jboss/as/server/shutdown/OperationListener.java new file mode 100644 index 00000000000..fa9704c313d --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/shutdown/OperationListener.java @@ -0,0 +1,31 @@ +package org.jboss.as.server.shutdown; + +/** + * + * Listener that is invoked to notify on success or failure of the + * + * @author Stuart Douglas + */ +public interface OperationListener { + + /** + * Invoked when a suspend operation is started. + */ + void suspendStarted(); + + /** + * Invoked when a suspend operation is complete + */ + void complete(); + + /** + * Invoked when a suspend operation is cancelled + */ + void cancelled(); + + /** + * Invoked when a suspend operation times out. + */ + void timeout(); + +} diff --git a/server/src/main/java/org/jboss/as/server/shutdown/ServerActivity.java b/server/src/main/java/org/jboss/as/server/shutdown/ServerActivity.java new file mode 100644 index 00000000000..d7bcc4cf0a2 --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/shutdown/ServerActivity.java @@ -0,0 +1,16 @@ +package org.jboss.as.server.shutdown; + +/** + * A server activity that may have to finish before the server can shut down gracefully. + * + * + * @author Stuart Douglas + */ +public interface ServerActivity { + + + void pause(ServerActivityListener listener); + + void resume(); + +} diff --git a/server/src/main/java/org/jboss/as/server/shutdown/ServerActivityListener.java b/server/src/main/java/org/jboss/as/server/shutdown/ServerActivityListener.java new file mode 100644 index 00000000000..905b7080a47 --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/shutdown/ServerActivityListener.java @@ -0,0 +1,20 @@ +package org.jboss.as.server.shutdown; + +/** + * Listener interface that is invoked when a system has suspended + * + * @author Stuart Douglas + */ +public interface ServerActivityListener { + + /** + * Method that is invoked when all paused requests have finished + */ + void requestsComplete(); + + /** + * Method that is invoked if the pause operation is cancelled. + */ + void unPaused(); + +} diff --git a/server/src/main/java/org/jboss/as/server/shutdown/SuspendController.java b/server/src/main/java/org/jboss/as/server/shutdown/SuspendController.java new file mode 100644 index 00000000000..c68ee46326c --- /dev/null +++ b/server/src/main/java/org/jboss/as/server/shutdown/SuspendController.java @@ -0,0 +1,175 @@ +package org.jboss.as.server.shutdown; + +import org.jboss.as.server.logging.ServerLogger; +import org.jboss.msc.service.Service; +import org.jboss.msc.service.ServiceName; +import org.jboss.msc.service.StartContext; +import org.jboss.msc.service.StartException; +import org.jboss.msc.service.StopContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +/** + * The graceful shutdown controller. This class co-ordinates the graceful shutdown and pause/resume of a + * servers operations. + *

+ *

+ * In most cases this work is delegated to the {@link org.jboss.as.server.requestcontroller.GlobalRequestController}, + * however for workflows that do no correspond directly to a request model a {@link ServerActivity} instance + * can be registered directly with this controller. + * + * @author Stuart Douglas + */ +public class SuspendController implements Service { + + public static final ServiceName SERVICE_NAME = ServiceName.JBOSS.append("server", "suspend-controller"); + + /** + * Timer that handles the timeout. We create it on pause, rather than leaving it hanging round. + */ + private Timer timer; + + private State state = State.RUNNING; + + private final List activities = new ArrayList<>(); + + private final List operationListeners = new ArrayList<>(); + + private int outstandingCount; + + private final ServerActivityListener listener = new ServerActivityListener() { + @Override + public void requestsComplete() { + activityPaused(); + } + + @Override + public void unPaused() { + activityResumed(); + } + }; + + + public synchronized void suspend(long timeoutMillis) { + ServerLogger.ROOT_LOGGER.suspendingServer(); + state = State.PAUSING; + //we iterate a copy, in case a listener tries to register a new listener + for(OperationListener listener: new ArrayList<>(operationListeners)) { + listener.suspendStarted(); + } + outstandingCount = activities.size(); + if (outstandingCount == 0) { + handlePause(); + } else { + for (ServerActivity activity : activities) { + activity.pause(this.listener); + } + timer = new Timer(); + if (timeoutMillis > 0) { + timer.schedule(new TimerTask() { + @Override + public void run() { + timeout(); + } + }, timeoutMillis); + } + } + } + + public synchronized void resume() { + if (state == State.RUNNING) { + return; + } + ServerLogger.ROOT_LOGGER.resumingServer(); + if (timer != null) { + timer.cancel(); + timer = null; + } + for(OperationListener listener: new ArrayList<>(operationListeners)) { + listener.cancelled(); + } + for (ServerActivity activity : activities) { + activity.resume(); + } + state = State.RUNNING; + } + + public synchronized void registerActivity(final ServerActivity activity) { + this.activities.add(activity); + } + + public synchronized void unRegisterActivity(final ServerActivity activity) { + this.activities.remove(activity); + } + + @Override + public synchronized void start(StartContext startContext) throws StartException { + state = State.RUNNING; + } + + @Override + public synchronized void stop(StopContext stopContext) { + } + + synchronized void activityPaused() { + --outstandingCount; + handlePause(); + } + + private void handlePause() { + state = State.PAUSED; + if (outstandingCount == 0) { + if (timer != null) { + timer.cancel(); + timer = null; + } + + for(OperationListener listener: new ArrayList<>(operationListeners)) { + listener.complete(); + } + } + } + + private synchronized void activityResumed() { + if (timer != null) { + timer.cancel(); + timer = null; + } + for(OperationListener listener: new ArrayList<>(operationListeners)) { + listener.cancelled(); + } + } + + synchronized void timeout() { + if (timer != null) { + timer.cancel(); + timer = null; + } + for(OperationListener listener: new ArrayList<>(operationListeners)) { + listener.timeout(); + } + } + + + public synchronized void addListener(final OperationListener listener) { + operationListeners.add(listener); + } + + public synchronized void removeListener(final OperationListener listener) { + operationListeners.remove(listener); + } + + @Override + public SuspendController getValue() throws IllegalStateException, IllegalArgumentException { + return this; + } + + public static enum State { + RUNNING, + PAUSING, + PAUSED + } +} diff --git a/server/src/main/resources/org/jboss/as/server/controller/descriptions/LocalDescriptions.properties b/server/src/main/resources/org/jboss/as/server/controller/descriptions/LocalDescriptions.properties index c1b04749a46..4cf334cb2a6 100644 --- a/server/src/main/resources/org/jboss/as/server/controller/descriptions/LocalDescriptions.properties +++ b/server/src/main/resources/org/jboss/as/server/controller/descriptions/LocalDescriptions.properties @@ -146,6 +146,10 @@ composite.steps=A list, where each item in the list has the same structure as a composite.result=A list, where each item in the list is the result for the equivalently positioned item in the 'steps' parameter list shutdown=Shuts down the server via a call to System.exit(0) shutdown.restart=If true, once shutdown the server will be restarted again +shutdown.timeout=The shutdown timeout in seconds. If this is zero (the default) then the server will shutdown immediately. A value larger than zero means the server will wait up to this many seconds for all active requests to finish. A value smaller than zero means that the server will wait indefinitely for all active requests to finish. +suspend=Suspends server operations gracefully. All current requests will complete normally, however no new requests will be accepted. +suspend.timeout=The timeout in seconds that a suspend operation will wait for the suspend operation to complete before returning. If this timeout passes the suspend operation returns, it does not resume normal operations. +resume=Resumes normal operations in a suspended server. dump-services=Dumps all services running in the container, including their status and dependency information. restart-required=Puts the server into a restart-required mode. server-set-restart-required=Puts the server into a restart-required mode. diff --git a/testsuite-core/domain/pom.xml b/testsuite-core/domain/pom.xml index b70ac001783..6f6485a0a6a 100644 --- a/testsuite-core/domain/pom.xml +++ b/testsuite-core/domain/pom.xml @@ -125,6 +125,7 @@ org/jboss/as/test/integration/respawn/*TestCase.java org/jboss/as/test/integration/domain/*TestCase.java org/jboss/as/test/integration/domain/suites/*TestSuite.java + org/jboss/as/test/integration/domain/suspendresume/*TestCase.java diff --git a/testsuite-core/domain/src/test/java/org/jboss/as/test/integration/domain/suites/DomainTestSuite.java b/testsuite-core/domain/src/test/java/org/jboss/as/test/integration/domain/suites/DomainTestSuite.java index 74d7704e463..ddc74508079 100644 --- a/testsuite-core/domain/src/test/java/org/jboss/as/test/integration/domain/suites/DomainTestSuite.java +++ b/testsuite-core/domain/src/test/java/org/jboss/as/test/integration/domain/suites/DomainTestSuite.java @@ -100,6 +100,7 @@ public static synchronized void beforeClass() { @AfterClass public static synchronized void afterClass() { stop(); + initializedLocally = false; } } diff --git a/testsuite-core/domain/src/test/java/org/jboss/as/test/integration/domain/suspendresume/DomainGracefulShutdownTestCase.java b/testsuite-core/domain/src/test/java/org/jboss/as/test/integration/domain/suspendresume/DomainGracefulShutdownTestCase.java new file mode 100644 index 00000000000..170eaf835bf --- /dev/null +++ b/testsuite-core/domain/src/test/java/org/jboss/as/test/integration/domain/suspendresume/DomainGracefulShutdownTestCase.java @@ -0,0 +1,178 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2011, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.jboss.as.test.integration.domain.suspendresume; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.jboss.as.controller.client.ModelControllerClient; +import org.jboss.as.controller.client.helpers.domain.DeploymentPlan; +import org.jboss.as.controller.client.helpers.domain.DeploymentPlanResult; +import org.jboss.as.controller.client.helpers.domain.DomainClient; +import org.jboss.as.controller.client.helpers.domain.DomainDeploymentManager; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.as.test.integration.common.HttpRequest; +import org.jboss.as.test.integration.domain.management.util.DomainLifecycleUtil; +import org.jboss.as.test.integration.domain.management.util.DomainTestSupport; +import org.jboss.as.test.integration.domain.management.util.DomainTestUtils; +import org.jboss.as.test.integration.domain.suites.DomainTestSuite; +import org.jboss.as.test.integration.management.util.MgmtOperationException; +import org.jboss.as.test.shared.TestSuiteEnvironment; +import org.jboss.dmr.ModelNode; +import org.jboss.msc.service.ServiceActivator; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.wildfly.test.suspendresumeendpoint.SuspendResumeHandler; +import org.wildfly.test.suspendresumeendpoint.TestSuspendServiceActivator; +import org.wildfly.test.suspendresumeendpoint.TestUndertowService; + +/** + * Test of suspend/resume in domain mode + * + * @author Stuart Douglas + */ +public class DomainGracefulShutdownTestCase { + public static final String WEB_SUSPEND_JAR = "web-suspend.jar"; + public static final String MAIN_SERVER_GROUP = "main-server-group"; + + private static DomainTestSupport testSupport; + private static DomainLifecycleUtil domainMasterLifecycleUtil; + private static DomainLifecycleUtil domainSlaveLifecycleUtil; + + @BeforeClass + public static void setupDomain() throws Exception { + testSupport = DomainTestSuite.createSupport(DomainGracefulShutdownTestCase.class.getSimpleName()); + domainMasterLifecycleUtil = testSupport.getDomainMasterLifecycleUtil(); + domainSlaveLifecycleUtil = testSupport.getDomainSlaveLifecycleUtil(); + } + + @AfterClass + public static void tearDownDomain() throws Exception { + testSupport = null; + domainMasterLifecycleUtil = null; + domainSlaveLifecycleUtil = null; + DomainTestSuite.stopSupport(); + } + + @Test + public void testGracefulShutdownDomainLevel() throws Exception { + + DomainClient client = domainMasterLifecycleUtil.getDomainClient(); + + DomainDeploymentManager deploymentManager = client.getDeploymentManager(); + DeploymentPlan plan = deploymentManager.newDeploymentPlan().add(WEB_SUSPEND_JAR, createDeployment().as(ZipExporter.class).exportAsInputStream()) + .andDeploy().toServerGroup(MAIN_SERVER_GROUP) + .build(); + DeploymentPlanResult res = deploymentManager.execute(plan).get(); + + + final String address = "http://" + TestSuiteEnvironment.getServerAddress() + ":8080/web-suspend"; + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + Future result = executorService.submit(new Callable() { + @Override + public Object call() throws Exception { + return HttpRequest.get(address, 60, TimeUnit.SECONDS); + } + }); + + Thread.sleep(1000); //nasty, but we need to make sure the HTTP request has started + + ModelNode op = new ModelNode(); + op.get(ModelDescriptionConstants.OP).set("stop-servers"); + op.get(ModelDescriptionConstants.TIMEOUT).set(60); + op.get(ModelDescriptionConstants.BLOCKING).set(false); + client.execute(op); + + //make sure requests are being rejected + final HttpURLConnection conn = (HttpURLConnection) new URL(address).openConnection(); + try { + conn.setDoInput(true); + int responseCode = conn.getResponseCode(); + Assert.assertEquals(503, responseCode); + } finally { + conn.disconnect(); + } + + //make sure the server is still up, and trigger the actual shutdown + HttpRequest.get(address + "?" + TestUndertowService.SKIP_GRACEFUL + "=true", 10, TimeUnit.SECONDS); + Assert.assertEquals(SuspendResumeHandler.TEXT, result.get()); + + //make sure our initial request completed + Assert.assertEquals(SuspendResumeHandler.TEXT, result.get()); + + + } finally { + ModelNode op = new ModelNode(); + op.get(ModelDescriptionConstants.OP).set("start-servers"); + client.execute(op); + + plan = deploymentManager.newDeploymentPlan().undeploy(WEB_SUSPEND_JAR) + .andRemoveUndeployed() + .toServerGroup(MAIN_SERVER_GROUP) + .build(); + res = deploymentManager.execute(plan).get(); + } + + + } + + private static ModelNode createOpNode(String address, String operation) { + ModelNode op = new ModelNode(); + + // set address + ModelNode list = op.get("address").setEmptyList(); + if (address != null) { + String[] pathSegments = address.split("/"); + for (String segment : pathSegments) { + String[] elements = segment.split("="); + list.add(elements[0], elements[1]); + } + } + op.get("operation").set(operation); + return op; + } + + private ModelNode executeOperation(final ModelNode op, final ModelControllerClient modelControllerClient) throws IOException, MgmtOperationException { + return DomainTestUtils.executeForResult(op, modelControllerClient); + } + + public static JavaArchive createDeployment() throws Exception { + JavaArchive jar = ShrinkWrap.create(JavaArchive.class, WEB_SUSPEND_JAR); + jar.addPackage(SuspendResumeHandler.class.getPackage()); + jar.addAsServiceProvider(ServiceActivator.class, TestSuspendServiceActivator.class); + jar.addAsResource(new StringAsset("Dependencies: org.jboss.dmr, org.jboss.as.controller, io.undertow.core, org.jboss.as.server, org.jboss.as.network\n"), "META-INF/MANIFEST.MF"); + return jar; + } +} diff --git a/testsuite-core/domain/src/test/java/org/jboss/as/test/integration/domain/suspendresume/DomainSuspendResumeTestCase.java b/testsuite-core/domain/src/test/java/org/jboss/as/test/integration/domain/suspendresume/DomainSuspendResumeTestCase.java new file mode 100644 index 00000000000..0658cf3b973 --- /dev/null +++ b/testsuite-core/domain/src/test/java/org/jboss/as/test/integration/domain/suspendresume/DomainSuspendResumeTestCase.java @@ -0,0 +1,185 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2011, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.jboss.as.test.integration.domain.suspendresume; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.jboss.as.controller.client.ModelControllerClient; +import org.jboss.as.controller.client.helpers.domain.DeploymentPlan; +import org.jboss.as.controller.client.helpers.domain.DeploymentPlanResult; +import org.jboss.as.controller.client.helpers.domain.DomainClient; +import org.jboss.as.controller.client.helpers.domain.DomainDeploymentManager; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.as.test.integration.common.HttpRequest; +import org.jboss.as.test.integration.domain.management.util.DomainLifecycleUtil; +import org.jboss.as.test.integration.domain.management.util.DomainTestSupport; +import org.jboss.as.test.integration.domain.management.util.DomainTestUtils; +import org.jboss.as.test.integration.domain.suites.DomainTestSuite; +import org.jboss.as.test.integration.management.util.MgmtOperationException; +import org.jboss.as.test.shared.TestSuiteEnvironment; +import org.jboss.dmr.ModelNode; +import org.jboss.msc.service.ServiceActivator; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.wildfly.test.suspendresumeendpoint.SuspendResumeHandler; +import org.wildfly.test.suspendresumeendpoint.TestSuspendServiceActivator; +import org.wildfly.test.suspendresumeendpoint.TestUndertowService; + +/** + * Test of suspend/resume in domain mode + * + * @author Stuart Douglas + */ +public class DomainSuspendResumeTestCase { + public static final String WEB_SUSPEND_JAR = "web-suspend.jar"; + public static final String MAIN_SERVER_GROUP = "main-server-group"; + + private static DomainTestSupport testSupport; + private static DomainLifecycleUtil domainMasterLifecycleUtil; + private static DomainLifecycleUtil domainSlaveLifecycleUtil; + + @BeforeClass + public static void setupDomain() throws Exception { + testSupport = DomainTestSuite.createSupport(DomainSuspendResumeTestCase.class.getSimpleName()); + domainMasterLifecycleUtil = testSupport.getDomainMasterLifecycleUtil(); + domainSlaveLifecycleUtil = testSupport.getDomainSlaveLifecycleUtil(); + } + + @AfterClass + public static void tearDownDomain() throws Exception { + testSupport = null; + domainMasterLifecycleUtil = null; + domainSlaveLifecycleUtil = null; + DomainTestSuite.stopSupport(); + } + + @Test + public void testSuspendResumeDomainLevel() throws Exception { + + DomainClient client = domainMasterLifecycleUtil.getDomainClient(); + + DomainDeploymentManager deploymentManager = client.getDeploymentManager(); + DeploymentPlan plan = deploymentManager.newDeploymentPlan().add(WEB_SUSPEND_JAR, createDeployment().as(ZipExporter.class).exportAsInputStream()) + .andDeploy().toServerGroup(MAIN_SERVER_GROUP) + .build(); + DeploymentPlanResult res = deploymentManager.execute(plan).get(); + + + final String address = "http://" + TestSuiteEnvironment.getServerAddress() + ":8080/web-suspend"; + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + Future result = executorService.submit(new Callable() { + @Override + public Object call() throws Exception { + return HttpRequest.get(address, 60, TimeUnit.SECONDS); + } + }); + + Thread.sleep(1000); //nasty, but we need to make sure the HTTP request has started + + ModelNode op = new ModelNode(); + op.get(ModelDescriptionConstants.OP).set("suspend-servers"); + client.execute(op); + + HttpRequest.get(address + "?" + TestUndertowService.SKIP_GRACEFUL + "=true", 10, TimeUnit.SECONDS); + Assert.assertEquals(SuspendResumeHandler.TEXT, result.get()); + + final HttpURLConnection conn = (HttpURLConnection) new URL(address).openConnection(); + try { + conn.setDoInput(true); + int responseCode = conn.getResponseCode(); + Assert.assertEquals(503, responseCode); + } finally { + conn.disconnect(); + } + + op = new ModelNode(); + op.get(ModelDescriptionConstants.OP).set("resume-servers"); + client.execute(op); + + Assert.assertEquals(SuspendResumeHandler.TEXT, HttpRequest.get(address, 60, TimeUnit.SECONDS)); + } finally { + try { + try { + HttpRequest.get(address + "?" + TestUndertowService.SKIP_GRACEFUL, 10, TimeUnit.SECONDS); + executorService.shutdown(); + + ModelNode op = new ModelNode(); + op.get(ModelDescriptionConstants.OP).set("resume"); + client.execute(op); + } finally { + plan = deploymentManager.newDeploymentPlan().undeploy(WEB_SUSPEND_JAR) + .andRemoveUndeployed() + .toServerGroup(MAIN_SERVER_GROUP) + .build(); + res = deploymentManager.execute(plan).get(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + + } + + private static ModelNode createOpNode(String address, String operation) { + ModelNode op = new ModelNode(); + + // set address + ModelNode list = op.get("address").setEmptyList(); + if (address != null) { + String[] pathSegments = address.split("/"); + for (String segment : pathSegments) { + String[] elements = segment.split("="); + list.add(elements[0], elements[1]); + } + } + op.get("operation").set(operation); + return op; + } + + private ModelNode executeOperation(final ModelNode op, final ModelControllerClient modelControllerClient) throws IOException, MgmtOperationException { + return DomainTestUtils.executeForResult(op, modelControllerClient); + } + + public static JavaArchive createDeployment() throws Exception { + JavaArchive jar = ShrinkWrap.create(JavaArchive.class, WEB_SUSPEND_JAR); + jar.addPackage(SuspendResumeHandler.class.getPackage()); + jar.addAsServiceProvider(ServiceActivator.class, TestSuspendServiceActivator.class); + jar.addAsResource(new StringAsset("Dependencies: org.jboss.dmr, org.jboss.as.controller, io.undertow.core, org.jboss.as.server, org.jboss.as.network\n"), "META-INF/MANIFEST.MF"); + return jar; + } +} diff --git a/testsuite-core/pom.xml b/testsuite-core/pom.xml index e16bd86ce71..3d8ea68d8ae 100644 --- a/testsuite-core/pom.xml +++ b/testsuite-core/pom.xml @@ -189,7 +189,6 @@ - diff --git a/testsuite-core/shared/src/main/java/org/wildfly/test/suspendresumeendpoint/SuspendResumeHandler.java b/testsuite-core/shared/src/main/java/org/wildfly/test/suspendresumeendpoint/SuspendResumeHandler.java new file mode 100644 index 00000000000..4c4e9e5ce17 --- /dev/null +++ b/testsuite-core/shared/src/main/java/org/wildfly/test/suspendresumeendpoint/SuspendResumeHandler.java @@ -0,0 +1,47 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2008, Red Hat Middleware LLC, and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.wildfly.test.suspendresumeendpoint; + +import java.util.concurrent.CountDownLatch; + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +public class SuspendResumeHandler implements HttpHandler { + public volatile CountDownLatch requestLatch = new CountDownLatch(1); + public static final String TEXT = "Running Request"; + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + if(exchange.getQueryParameters().containsKey(TestUndertowService.SKIP_GRACEFUL)) { + requestLatch.countDown(); + return; + } + + try { + requestLatch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + exchange.getResponseSender().send(TEXT); + } +} diff --git a/testsuite-core/shared/src/main/java/org/wildfly/test/suspendresumeendpoint/TestSuspendServiceActivator.java b/testsuite-core/shared/src/main/java/org/wildfly/test/suspendresumeendpoint/TestSuspendServiceActivator.java new file mode 100644 index 00000000000..3ccbc4bc528 --- /dev/null +++ b/testsuite-core/shared/src/main/java/org/wildfly/test/suspendresumeendpoint/TestSuspendServiceActivator.java @@ -0,0 +1,23 @@ +package org.wildfly.test.suspendresumeendpoint; + +import org.jboss.as.network.SocketBindingManager; +import org.jboss.as.server.requestcontroller.GlobalRequestController; +import org.jboss.msc.service.ServiceActivator; +import org.jboss.msc.service.ServiceActivatorContext; +import org.jboss.msc.service.ServiceRegistryException; + +/** + * @author Stuart Douglas + */ +public class TestSuspendServiceActivator implements ServiceActivator { + + @Override + public void activate(ServiceActivatorContext serviceActivatorContext) throws ServiceRegistryException { + + TestUndertowService testUndertowService = new TestUndertowService(); + serviceActivatorContext.getServiceTarget().addService(TestUndertowService.SERVICE_NAME, testUndertowService) + .addDependency(GlobalRequestController.SERVICE_NAME, GlobalRequestController.class, testUndertowService.getRequestControllerInjectedValue()) + .addDependency(SocketBindingManager.SOCKET_BINDING_MANAGER, SocketBindingManager.class, testUndertowService.getSocketBindingManagerInjectedValue()) + .install(); + } +} diff --git a/testsuite-core/shared/src/main/java/org/wildfly/test/suspendresumeendpoint/TestUndertowService.java b/testsuite-core/shared/src/main/java/org/wildfly/test/suspendresumeendpoint/TestUndertowService.java new file mode 100644 index 00000000000..9431ff11e38 --- /dev/null +++ b/testsuite-core/shared/src/main/java/org/wildfly/test/suspendresumeendpoint/TestUndertowService.java @@ -0,0 +1,95 @@ +package org.wildfly.test.suspendresumeendpoint; + +import java.util.concurrent.atomic.AtomicInteger; +import org.jboss.as.network.SocketBindingManager; +import org.jboss.as.server.requestcontroller.ControlPoint; +import org.jboss.as.server.requestcontroller.GlobalRequestController; +import org.jboss.as.server.requestcontroller.RunResult; +import org.jboss.msc.service.Service; +import org.jboss.msc.service.ServiceName; +import org.jboss.msc.service.StartContext; +import org.jboss.msc.service.StartException; +import org.jboss.msc.service.StopContext; +import org.jboss.msc.value.InjectedValue; + +import io.undertow.Undertow; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +public class TestUndertowService implements Service { + + + private static final AtomicInteger COUNT = new AtomicInteger(); + + + public static final ServiceName SERVICE_NAME = ServiceName.JBOSS.append("test-undertow-server"); + public static final String SKIP_GRACEFUL = "skip-graceful"; + + private volatile Undertow undertow; + private final InjectedValue requestControllerInjectedValue = new InjectedValue<>(); + private final InjectedValue socketBindingManagerInjectedValue = new InjectedValue<>(); + + @Override + public void start(StartContext context) throws StartException { //add graceful shutdown support + final SuspendResumeHandler suspendResumeHandler = new SuspendResumeHandler(); + final ControlPoint controlPoint = requestControllerInjectedValue.getValue().getEntryPoint("test", "test"); + HttpHandler shutdown = new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + if(exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + final int count = COUNT.incrementAndGet(); + if(exchange.getQueryParameters().containsKey(SKIP_GRACEFUL)) { + //bit of a hack, allows to send in some requests even when the server is paused + //very useful for testing + System.out.println("Skipping request " + count + " " + exchange); + suspendResumeHandler.handleRequest(exchange); + return; + } + System.out.println("Attempting " + count + " " + exchange); + RunResult result = controlPoint.beginRequest(); + if (result == RunResult.REJECTED) { + System.out.println("Rejected " + count + " " + exchange); + exchange.setResponseCode(503); + return; + } + exchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + System.out.println("Completed " + count + " " + exchange); + controlPoint.requestComplete(); + nextListener.proceed(); + } + }); + suspendResumeHandler.handleRequest(exchange); + } + }; + undertow = Undertow.builder().addHttpListener(8080 + socketBindingManagerInjectedValue.getValue().getPortOffset(), "0.0.0.0").setHandler(shutdown).build(); + undertow.start(); + } + + @Override + public void stop(StopContext context) { + undertow.stop(); + undertow = null; + } + + @Override + public TestUndertowService getValue() throws IllegalStateException, IllegalArgumentException { + return this; + } + + public InjectedValue getRequestControllerInjectedValue() { + return requestControllerInjectedValue; + } + + public InjectedValue getSocketBindingManagerInjectedValue() { + return socketBindingManagerInjectedValue; + } +} diff --git a/testsuite-core/standalone/src/test/java/org/wildfly/core/test/standalone/suspend/web/SuspendResumeTestCase.java b/testsuite-core/standalone/src/test/java/org/wildfly/core/test/standalone/suspend/web/SuspendResumeTestCase.java new file mode 100644 index 00000000000..6326797a16a --- /dev/null +++ b/testsuite-core/standalone/src/test/java/org/wildfly/core/test/standalone/suspend/web/SuspendResumeTestCase.java @@ -0,0 +1,129 @@ + /* + * JBoss, Home of Professional Open Source. + * Copyright 2008, Red Hat Middleware LLC, and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.wildfly.core.test.standalone.suspend.web; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import org.jboss.as.controller.client.helpers.standalone.ServerDeploymentHelper; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.as.test.integration.common.HttpRequest; +import org.jboss.as.test.shared.TestSuiteEnvironment; +import org.jboss.dmr.ModelNode; +import org.jboss.msc.service.ServiceActivator; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.wildfly.core.testrunner.ManagementClient; +import org.wildfly.core.testrunner.WildflyTestRunner; +import org.wildfly.test.suspendresumeendpoint.SuspendResumeHandler; +import org.wildfly.test.suspendresumeendpoint.TestSuspendServiceActivator; +import org.wildfly.test.suspendresumeendpoint.TestUndertowService; + +/** + * Tests for suspend/resume functionality + */ +@RunWith(WildflyTestRunner.class) +public class SuspendResumeTestCase { + + public static final String WEB_SUSPEND_JAR = "web-suspend.jar"; + @Inject + private static ManagementClient managementClient; + + @BeforeClass + public static void deploy() throws Exception { + ServerDeploymentHelper helper = new ServerDeploymentHelper(managementClient.getControllerClient()); + JavaArchive war = ShrinkWrap.create(JavaArchive.class, WEB_SUSPEND_JAR); + war.addPackage(SuspendResumeHandler.class.getPackage()); + war.addAsServiceProvider(ServiceActivator.class, TestSuspendServiceActivator.class); + war.addAsResource(new StringAsset("Dependencies: org.jboss.dmr, org.jboss.as.controller, io.undertow.core, org.jboss.as.server, org.jboss.as.network\n"), "META-INF/MANIFEST.MF"); + helper.deploy(WEB_SUSPEND_JAR, war.as(ZipExporter.class).exportAsInputStream()); + + } + + @AfterClass + public static void undeploy() throws ServerDeploymentHelper.ServerDeploymentException { + ServerDeploymentHelper helper = new ServerDeploymentHelper(managementClient.getControllerClient()); + helper.undeploy(WEB_SUSPEND_JAR); + } + + @Test + public void testSuspendResume() throws Exception { + + final String address = "http://" + TestSuiteEnvironment.getServerAddress() + ":8080/web-suspend"; + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + + Future result = executorService.submit(new Callable() { + @Override + public Object call() throws Exception { + return HttpRequest.get(address, 60, TimeUnit.SECONDS); + } + }); + + Thread.sleep(1000); //nasty, but we need to make sure the HTTP request has started + + ModelNode op = new ModelNode(); + op.get(ModelDescriptionConstants.OP).set("suspend"); + managementClient.getControllerClient().execute(op); + + HttpRequest.get(address + "?" + TestUndertowService.SKIP_GRACEFUL + "=true", 10, TimeUnit.SECONDS); + Assert.assertEquals(SuspendResumeHandler.TEXT, result.get()); + + final HttpURLConnection conn = (HttpURLConnection) new URL(address).openConnection(); + try { + conn.setDoInput(true); + int responseCode = conn.getResponseCode(); + Assert.assertEquals(503, responseCode); + } finally { + conn.disconnect(); + } + + op = new ModelNode(); + op.get(ModelDescriptionConstants.OP).set("resume"); + managementClient.getControllerClient().execute(op); + + Assert.assertEquals(SuspendResumeHandler.TEXT, HttpRequest.get(address, 60, TimeUnit.SECONDS)); + } finally { + HttpRequest.get(address + "?" + TestUndertowService.SKIP_GRACEFUL, 10, TimeUnit.SECONDS); + executorService.shutdown(); + + ModelNode op = new ModelNode(); + op.get(ModelDescriptionConstants.OP).set("resume"); + managementClient.getControllerClient().execute(op); + } + + + } + +}