/*
* JBoss, Home of Professional Open Source.
* Copyright 2016, 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.wildfly.extension.core.management;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.jboss.as.controller.ControlledProcessState;
import org.jboss.as.controller.ControlledProcessStateService;
import org.jboss.as.controller.ProcessType;
import org.jboss.as.controller.RunningMode;
import org.jboss.as.server.Services;
import org.jboss.as.server.suspend.OperationListener;
import org.jboss.as.server.suspend.SuspendController;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.ServiceBuilder;
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;
import org.wildfly.extension.core.management.client.Process;
import org.wildfly.extension.core.management.client.RuntimeConfigurationStateChangeEvent;
import org.wildfly.extension.core.management.client.RunningStateChangeEvent;
import org.wildfly.extension.core.management.logging.CoreManagementLogger;
import org.wildfly.extension.core.management.client.ProcessStateListener;
import org.wildfly.extension.core.management.client.ProcessStateListenerInitParameters;
/**
* Service that listens for process state changes and notifies the ProcessStateListener instance of those events.
* That means when the running state or the runtime configuration state changes.
* <strong>RunningStateChangeEvent for suspend / resume can only be sent for servers (somain or standalone) but not on HostControllers.</strong>
*
* @author <a href="http://jmesnil.net/">Jeff Mesnil</a> (c) 2016 Red Hat inc.
* @author <a href="mailto:ehugonne@redhat.com">Emmanuel Hugonnet</a> (c) 2016 Red Hat, inc.
*/
public class ProcessStateListenerService implements Service<Void> {
static final ServiceName SERVICE_NAME = ServiceName.JBOSS.append("core", "management", "process-state-listener");
private final InjectedValue<ControlledProcessStateService> controlledProcessStateService = new InjectedValue<>();
private final InjectedValue<SuspendController> suspendControllerInjectedValue = new InjectedValue<>();
private final InjectedValue<ExecutorService> executorServiceValue = new InjectedValue<>();
private final PropertyChangeListener propertyChangeListener;
private final OperationListener operationListener;
private final ProcessStateListener listener;
private final ProcessStateListenerInitParameters parameters;
private final String name;
private final int timeout;
private final ProcessType processType;
private volatile Process.RunningState runningState = null;
public ProcessStateListenerService(ProcessType processType, RunningMode runningMode, String name, ProcessStateListener listener, Map<String, String> properties, int timeout) {
this.listener = listener;
this.name = name;
this.timeout = timeout;
this.processType = processType;
this.parameters = new ProcessStateListenerInitParameters.Builder()
.setInitProperties(properties)
.setRunningMode(Process.RunningMode.from(runningMode.name()))
.setProcessType(Process.Type.valueOf(processType.name()))
.build();
this.propertyChangeListener = (PropertyChangeEvent evt) -> {
if ("currentState".equals(evt.getPropertyName())) {
Process.RuntimeConfigurationState oldState = Process.RuntimeConfigurationState.valueOf(((ControlledProcessState.State) evt.getOldValue()).name());
Process.RuntimeConfigurationState newState = Process.RuntimeConfigurationState.valueOf(((ControlledProcessState.State) evt.getNewValue()).name());
transition(oldState, newState);
}
};
if (processType != ProcessType.HOST_CONTROLLER && processType != ProcessType.EMBEDDED_HOST_CONTROLLER) {
this.operationListener = new OperationListener() {
@Override
public void suspendStarted() {
suspendTransition(runningState, Process.RunningState.SUSPENDING);
}
@Override
public void complete() {
suspendTransition(runningState, Process.RunningState.SUSPENDED);
}
@Override
public void cancelled() {
if(runningState == null) {//gracefull startup
suspendTransition(Process.RunningState.STARTING, Process.RunningState.SUSPENDED);
return;
}
switch (runningMode) {
case ADMIN_ONLY:
suspendTransition(runningState, Process.RunningState.ADMIN_ONLY);
break;
case NORMAL:
suspendTransition(runningState, Process.RunningState.NORMAL);
break;
}
}
@Override
public void timeout() {
}
};
} else {
operationListener = null;
}
}
private void transition(Process.RuntimeConfigurationState oldState, Process.RuntimeConfigurationState newState) {
if(oldState == newState) {
return;
}
if (runningState == null) {
switch (oldState) {
case RUNNING:
if (processType.isServer()) {
runningState = Process.RunningState.SUSPENDED;
} else {
if (parameters.getRunningMode() == Process.RunningMode.NORMAL) {
runningState = Process.RunningState.NORMAL;
} else {
runningState = Process.RunningState.ADMIN_ONLY;
}
}
break;
case STARTING:
runningState = Process.RunningState.STARTING;
break;
case STOPPING:
runningState = Process.RunningState.STOPPING;
break;
case STOPPED:
runningState = Process.RunningState.STOPPED;
break;
case RELOAD_REQUIRED:
case RESTART_REQUIRED:
default:
}
}
Future<?> controlledProcessStateTransition = executorServiceValue.getValue().submit(() -> {
listener.runtimeConfigurationStateChanged(new RuntimeConfigurationStateChangeEvent(oldState, newState));
});
try {
controlledProcessStateTransition.get(timeout, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
CoreManagementLogger.ROOT_LOGGER.processStateInvokationError(ex, name);
} catch (TimeoutException ex) {
CoreManagementLogger.ROOT_LOGGER.processStateTimeoutError(ex, name);
} catch (ExecutionException | RuntimeException t) {
CoreManagementLogger.ROOT_LOGGER.processStateInvokationError(t, name);
}
switch(newState) {
case RUNNING:
if (Process.RunningState.NORMAL != runningState && Process.RunningState.ADMIN_ONLY != runningState) {
if (parameters.getRunningMode() == Process.RunningMode.NORMAL) {
suspendTransition(runningState, Process.RunningState.NORMAL);
} else {
suspendTransition(runningState, Process.RunningState.ADMIN_ONLY);
}
}
break;
case STARTING:
if (Process.RunningState.STARTING != runningState) {
suspendTransition(runningState, Process.RunningState.STARTING);
}
break;
case STOPPING:
if (Process.RunningState.STOPPING != runningState) {
suspendTransition(runningState, Process.RunningState.STOPPING);
}
break;
case STOPPED:
if (Process.RunningState.STOPPED != runningState) {
suspendTransition(runningState, Process.RunningState.STOPPED);
}
break;
case RELOAD_REQUIRED:
case RESTART_REQUIRED:
default:
}
}
/**
* This will <strong>NEVER</strong> be called on a HostController.
* @param newState the new running state.
*/
private void suspendTransition(Process.RunningState oldState, Process.RunningState newState) {
if(oldState == newState) {
return;
}
this.runningState = newState;
Future<?> suspendStateTransition = executorServiceValue.getValue().submit(() -> {
listener.runningStateChanged(new RunningStateChangeEvent(oldState, newState));
});
try {
suspendStateTransition.get(timeout, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
CoreManagementLogger.ROOT_LOGGER.processStateInvokationError(ex, name);
} catch (TimeoutException ex) {
CoreManagementLogger.ROOT_LOGGER.processStateTimeoutError(ex, name);
} catch (ExecutionException | RuntimeException t) {
CoreManagementLogger.ROOT_LOGGER.processStateInvokationError(t, name);
}
}
static void install(ServiceTarget serviceTarget, ProcessType processType, RunningMode runningMode, String listenerName, ProcessStateListener listener, Map<String, String> properties, int timeout) {
ProcessStateListenerService service = new ProcessStateListenerService(processType, runningMode, listenerName, listener, properties, timeout);
ServiceBuilder<Void> builder = serviceTarget.addService(SERVICE_NAME.append(listenerName), service)
.addDependency(ControlledProcessStateService.SERVICE_NAME, ControlledProcessStateService.class, service.controlledProcessStateService);
if (processType != ProcessType.HOST_CONTROLLER && processType != ProcessType.EMBEDDED_HOST_CONTROLLER) {
builder.addDependency(SuspendController.SERVICE_NAME, SuspendController.class, service.suspendControllerInjectedValue);
builder.addDependency(Services.JBOSS_SERVER_EXECUTOR, ExecutorService.class, service.executorServiceValue);
} else {
builder.addDependency(ServiceName.JBOSS.append("host", "controller", "executor"), ExecutorService.class, service.executorServiceValue);
}
builder.install();
}
@Override
public void start(StartContext context) throws StartException {
Runnable task = () -> {
try {
ProcessStateListenerService.this.listener.init(parameters);
SuspendController controller = ProcessStateListenerService.this.suspendControllerInjectedValue.getOptionalValue();
if (controller != null) {
switch (controller.getState()) {
case PRE_SUSPEND:
this.runningState = Process.RunningState.PRE_SUSPEND;
break;
case RUNNING:
if (parameters.getRunningMode() == Process.RunningMode.NORMAL) {
this.runningState = Process.RunningState.NORMAL;
} else {
this.runningState = Process.RunningState.ADMIN_ONLY;
}
break;
case SUSPENDED:
this.runningState = Process.RunningState.SUSPENDED;
break;
case SUSPENDING:
this.runningState = Process.RunningState.SUSPENDING;
break;
}
controller.addListener(operationListener);
}
controlledProcessStateService.getValue().addPropertyChangeListener(propertyChangeListener);
context.complete();
} catch (RuntimeException t) {
context.failed(new StartException(CoreManagementLogger.ROOT_LOGGER.processStateInitError(t, name)));
}
};
try {
executorServiceValue.getValue().execute(task);
} catch (RejectedExecutionException e) {
task.run();
} finally {
context.asynchronous();
}
}
@Override
public void stop(StopContext context) {
Runnable task = () -> {
controlledProcessStateService.getValue().removePropertyChangeListener(propertyChangeListener);
runningState = null;
try {
listener.cleanup();
SuspendController controller = suspendControllerInjectedValue.getOptionalValue();
if (controller != null) {
controller.removeListener(operationListener);
}
} catch (RuntimeException t) {
CoreManagementLogger.ROOT_LOGGER.processStateCleanupError(t, name);
} finally {
context.complete();
}
};
final ExecutorService executorService = executorServiceValue.getValue();
try {
try {
executorService.execute(task);
} catch (RejectedExecutionException e) {
task.run();
}
} finally {
context.asynchronous();
}
}
@Override
public Void getValue() throws IllegalStateException, IllegalArgumentException {
return null;
}
}