package org.jboss.as.server.suspend; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CORE_SERVICE; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MANAGEMENT; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MANAGEMENT_OPERATIONS; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SERVICE; import org.jboss.as.controller.ModelController; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.descriptions.ModelDescriptionConstants; import org.jboss.as.controller.notification.Notification; import org.jboss.as.controller.notification.NotificationFilter; import org.jboss.as.controller.notification.NotificationHandler; 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 org.jboss.msc.value.InjectedValue; 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. * <p/> * <p/> * In most cases this work is delegated to the request controller subsystem. * 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<SuspendController> { //TODO: should this notification handling be placed into its own class private static final PathAddress NOTIFICATION_ADDRESS = PathAddress.pathAddress(CORE_SERVICE, MANAGEMENT).append(SERVICE, MANAGEMENT_OPERATIONS); 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.SUSPENDED; private final List<ServerActivity> activities = new ArrayList<>(); private final List<OperationListener> operationListeners = new ArrayList<>(); private final InjectedValue<ModelController> modelControllerInjectedValue = new InjectedValue<>(); private int outstandingCount; private boolean startSuspended; private final ServerActivityCallback listener = () -> activityPaused(); public SuspendController() { this.startSuspended = false; } public void setStartSuspended(boolean startSuspended) { this.startSuspended = startSuspended; state = State.SUSPENDED; } public synchronized void suspend(long timeoutMillis) { if (timeoutMillis > 0) { ServerLogger.ROOT_LOGGER.suspendingServer(timeoutMillis); } else { ServerLogger.ROOT_LOGGER.suspendingServerWithNoTimeout(); } state = State.PRE_SUSPEND; //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 { CountingRequestCountCallback cb = new CountingRequestCountCallback(outstandingCount, () -> { state = State.SUSPENDING; for (ServerActivity activity : activities) { activity.suspended(SuspendController.this.listener); } }); for (ServerActivity activity : activities) { activity.preSuspend(cb); } 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) { try { activity.resume(); } catch (Exception e) { ServerLogger.ROOT_LOGGER.failedToResume(activity); } } state = State.RUNNING; } public synchronized void registerActivity(final ServerActivity activity) { this.activities.add(activity); if(state != State.RUNNING) { //if the activity is added when we are not running we just immediately suspend it //this should only happen at boot, so there should be no outstanding requests anyway activity.suspended(() -> { }); } } public synchronized void unRegisterActivity(final ServerActivity activity) { this.activities.remove(activity); } @Override public synchronized void start(StartContext startContext) throws StartException { if(!startSuspended) { final NotificationFilter filter = notification -> notification.getType().equals(ModelDescriptionConstants.BOOT_COMPLETE_NOTIFICATION); NotificationHandler handler = new NotificationHandler() { @Override public void handleNotification(Notification notification) { resume(); modelControllerInjectedValue.getValue().getNotificationRegistry().unregisterNotificationHandler(NOTIFICATION_ADDRESS, this, filter); } }; modelControllerInjectedValue.getValue().getNotificationRegistry().registerNotificationHandler(NOTIFICATION_ADDRESS, handler, filter); //if the service bounces we don't want to auto resume if we are suspended startSuspended = true; } else { ServerLogger.AS_ROOT_LOGGER.startingServerSuspended(); } } @Override public synchronized void stop(StopContext stopContext) { } public State getState() { return state; } synchronized void activityPaused() { --outstandingCount; handlePause(); } private void handlePause() { if (outstandingCount == 0) { state = State.SUSPENDED; if (timer != null) { timer.cancel(); timer = null; } for(OperationListener listener: new ArrayList<>(operationListeners)) { listener.complete(); } } } 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 InjectedValue<ModelController> getModelControllerInjectedValue() { return modelControllerInjectedValue; } public enum State { RUNNING, PRE_SUSPEND, SUSPENDING, SUSPENDED } }