/*******************************************************************************
* Copyright (c) 2011 GigaSpaces Technologies Ltd. All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package org.cloudifysource.usm.dsl;
import groovy.lang.Closure;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import javax.activation.UnsupportedDataTypeException;
import javax.annotation.PostConstruct;
import org.cloudifysource.domain.LifecycleEvents;
import org.cloudifysource.domain.PluginDescriptor;
import org.cloudifysource.domain.Service;
import org.cloudifysource.domain.context.ServiceContext;
import org.cloudifysource.domain.entry.ExecutableDSLEntry;
import org.cloudifysource.dsl.entry.ClosureExecutableEntry;
import org.cloudifysource.dsl.entry.ExecutableDSLEntryFactory;
import org.cloudifysource.dsl.internal.CloudifyConstants;
import org.cloudifysource.dsl.internal.DSLValidationException;
import org.cloudifysource.usm.Plugin;
import org.cloudifysource.usm.TCPPortEventListener;
import org.cloudifysource.usm.USMComponent;
import org.cloudifysource.usm.USMException;
import org.cloudifysource.usm.USMUtils;
import org.cloudifysource.usm.UniversalServiceManagerBean;
import org.cloudifysource.usm.details.Details;
import org.cloudifysource.usm.details.DetailsException;
import org.cloudifysource.usm.details.ProcessDetails;
import org.cloudifysource.usm.events.EventResult;
import org.cloudifysource.usm.events.USMEvent;
import org.cloudifysource.usm.launcher.DefaultProcessLauncher;
import org.cloudifysource.usm.launcher.ProcessLauncher;
import org.cloudifysource.usm.liveness.LivenessDetector;
import org.cloudifysource.usm.locator.DefaultProcessLocator;
import org.cloudifysource.usm.locator.ProcessLocator;
import org.cloudifysource.usm.locator.ProcessLocatorExecutor;
import org.cloudifysource.usm.monitors.Monitor;
import org.cloudifysource.usm.monitors.MonitorException;
import org.cloudifysource.usm.monitors.process.ProcessMonitor;
import org.cloudifysource.usm.shutdown.DefaultProcessKiller;
import org.cloudifysource.usm.shutdown.DefaultStop;
import org.cloudifysource.usm.shutdown.ProcessKiller;
import org.cloudifysource.usm.stopDetection.ProcessStopDetector;
import org.cloudifysource.usm.stopDetection.StopDetector;
import org.openspaces.pu.container.support.ResourceApplicationContext;
import org.openspaces.ui.UserInterface;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/*****************
* The Spring Bean configuration for the USM - Spring components are initialized in this one class.
*
* @author barakme
* @since 2.0.0
*
*/
@Configuration
public class DSLBeanConfiguration implements ApplicationContextAware {
@Autowired(required = false)
private ServiceConfiguration configuration;
private boolean active;
private ResourceApplicationContext context;
private Service service;
private File puExtDir;
private ProcessLauncher launcher;
private ServiceContext serviceContext;
// There is lots of boiler plate code here - ignore checkstyle.
// CHECKSTYLE:OFF
@PostConstruct
public void init() {
this.active = configuration instanceof ServiceConfiguration;
if (this.active) {
this.service = configuration.getService();
this.puExtDir = configuration.getPuExtDir();
this.serviceContext = configuration.getServiceContext();
}
}
@Bean
public USMComponent getDslDetails() {
if (!active) {
return null;
}
return new DSLDetails();
}
@Bean
public ProcessLauncher getDslLauncher() {
if (!active) {
return null;
}
final ProcessLauncher retval = (ProcessLauncher) createBeanIfNotExistsType(new DefaultProcessLauncher(),
ProcessLauncher.class);
if (retval != null) {
this.launcher = retval;
} else {
final Collection<ProcessLauncher> launchers =
this.context.getBeanFactory().getBeansOfType(ProcessLauncher.class).values();
if (launchers.isEmpty()) {
throw new IllegalStateException("No ProcessLauncher was found in Context!");
}
this.launcher = launchers.iterator().next();
}
return retval;
}
@Bean
public ProcessKiller getDslKiller() {
return (ProcessKiller) createBeanIfNotExistsType(new DefaultProcessKiller(),
ProcessKiller.class);
}
@Bean
public USMComponent getDslProcessMonitor() {
if (!active) {
return null;
}
return new ProcessMonitor();
}
@Bean
public USMComponent getDslProcessDetails() {
if (!active) {
return null;
}
return new ProcessDetails();
}
@Bean
public USMComponent getDslEventCommand() {
if (!active) {
return null;
}
return new DSLCommandsLifecycleListener();
}
@Bean
public USMComponent getDslUIDetails()
throws UnsupportedDataTypeException, IllegalAccessException, InvocationTargetException {
if (!active) {
return null;
}
if (this.service.getUserInterface() == null) {
return null;
}
//Create openspaces UI object and convert it to support backwards compatibility
UserInterfaceConverter converter = new UserInterfaceConverter();
UserInterface convertUserInterface = converter.convertUserInterface(this.service.getUserInterface());
return new UIDetails(convertUserInterface);
}
@Bean
public USMComponent getDslPlugins() {
if (!active) {
return null;
}
final List<PluginDescriptor> plugins = this.service.getPlugins();
if (plugins == null || plugins.isEmpty()) {
return null;
}
for (final PluginDescriptor descriptor : plugins) {
final Class<?> pluginClass = getPluginClass(descriptor);
if (pluginClass != null) {
final String name = descriptor.getName() == null ? descriptor.getClassName() : descriptor.getName();
try {
this.context.getBeanFactory().getBean(name);
throw new IllegalArgumentException("The name: " + name
+ " is already in use and can't be used to identify a plugin");
} catch (final NoSuchBeanDefinitionException e) {
// ignore - this is the expected result
}
// Add the bean definition to the application context
((DefaultListableBeanFactory) this.context.getBeanFactory()).registerBeanDefinition(pluginClass
.getSimpleName(), BeanDefinitionBuilder.rootBeanDefinition(pluginClass.getName())
.getBeanDefinition());
// Initialize the bean
final Object pluginObject = this.context.getBeanFactory().getBean(pluginClass);
final Plugin component = (Plugin) pluginObject;
component.setServiceContext(this.serviceContext);
component.setConfig(descriptor.getConfig());
}
}
return null;
}
private static java.util.logging.Logger logger = java.util.logging.Logger.getLogger(DSLBeanConfiguration.class
.getName());
private Class<?> getPluginClass(final PluginDescriptor descriptor) {
Class<?> clazz = null;
try {
final String className = descriptor.getClassName();
if (className == null || className.length() == 0) {
throw new IllegalArgumentException("Plugin must have a class name");
}
clazz = Class.forName(className);
} catch (final ClassNotFoundException e) {
logger.log(Level.SEVERE,
"Class " + descriptor.getClassName() + " for Plugin was not found",
e);
throw new IllegalArgumentException("Could not find class: " + descriptor.getClassName() + " for plugin "
+ descriptor.getName(), e);
}
if (!USMComponent.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Plugin of class: " + descriptor.getClassName()
+ " does not implement the USMComponent interface");
}
return clazz;
}
private USMComponent createBeanIfNotExistsType(final USMComponent bean, final Class<?> typeToCheck) {
if (!active) {
return null;
}
if (typeToCheck == null || this.context.getBeanFactory().getBeansOfType(typeToCheck).isEmpty()) {
return bean;
}
return null;
}
@Bean
public USMComponent getDetails() {
final Object details = this.service.getLifecycle().getDetails();
if (details == null) {
return null;
}
return new Details() {
@SuppressWarnings("unchecked")
@Override
public Map<String, Object> getDetails(final UniversalServiceManagerBean usm,
final ServiceConfiguration config)
throws DetailsException {
try {
final Map<String, Object> returnMap = new HashMap<String, Object>();
// Map can appear in 2 forms. As a map or as a closure that
// returns a map
logger.fine("getting Details map from details closure");
if (details instanceof Map<?, ?>) {
for (final Map.Entry<String, Object> entry : ((Map<String, Object>) details).entrySet()) {
if (entry.getValue() instanceof Closure) {
final EventResult value =
new DSLEntryExecutor(ExecutableDSLEntryFactory.createEntry(entry.getValue(),
"details", puExtDir), launcher, puExtDir, LifecycleEvents.SERVICE_DETAILS).run();
if (value.isSuccess()) {
returnMap.put(entry.getKey(),
value.getResult());
} else {
logger.warning("Failed to execute closure with key value of " + entry.getKey());
}
} else {
returnMap.put(entry.getKey(),
entry.getValue());
}
}
} else if (details instanceof Closure) {
final EventResult result =
new DSLEntryExecutor(
ExecutableDSLEntryFactory.createEntry(details, "details", puExtDir),
launcher, puExtDir, LifecycleEvents.SERVICE_DETAILS).run();
if (result.isSuccess()) {
returnMap.putAll((Map<String, Object>) result.getResult());
}
}
return returnMap;
} catch (final Exception e) {
return null;
}
}
};
}
@Bean
public USMComponent getMonitor() {
final Object monitor = this.service.getLifecycle().getMonitors();
if (monitor == null) {
return null;
}
if (monitor instanceof Closure<?>) {
return new Monitor() {
@SuppressWarnings("unchecked")
@Override
public Map<String, Number> getMonitorValues(final UniversalServiceManagerBean usm,
final ServiceConfiguration config)
throws MonitorException {
final Object obj = ((Closure<?>) monitor).call();
if (obj instanceof Map<?, ?>) {
return USMUtils.convertMapToNumericValues((Map<String, Object>) obj);
}
throw new IllegalArgumentException(
"The Monitor closure defined in the DSL file does not evaluate to a Map! "
+ "Received object was of type: " + obj.getClass().getName());
}
};
}
// else if the monitor is of type Map we run all of the map's values
// as closures and return the output map.
return new Monitor() {
@SuppressWarnings("unchecked")
@Override
public Map<String, Number> getMonitorValues(final UniversalServiceManagerBean usm,
final ServiceConfiguration config)
throws MonitorException {
final Map<String, Object> returnMap = new HashMap<String, Object>();
if (monitor instanceof Map<?, ?>) {
for (final Map.Entry<String, Object> entryObject : ((Map<String, Object>) monitor).entrySet()) {
final Object object = entryObject.getValue();
EventResult result;
try {
result =
new DSLEntryExecutor(
ExecutableDSLEntryFactory.createEntry(object, entryObject.getKey(),
puExtDir), launcher,
puExtDir, LifecycleEvents.SERVICE_MONITORS).run();
} catch (final DSLValidationException e) {
throw new MonitorException("Executable entry in monitor is invalid", e);
}
if (!result.isSuccess()) {
logger.log(Level.WARNING,
"DSL Entry failed to execute: " + result.getException());
} else {
returnMap.put(entryObject.getKey(),
result.getResult());
}
}
}
return USMUtils.convertMapToNumericValues(returnMap);
}
};
}
// The sigar based process detection is problematic. When a process dies, sigar sometimes does not detect the death.
// Worse, the sigar API requests may actually get stuck, locking up the stop detection thread.
@Bean
public USMEvent getProcessStopDetection() {
boolean enabled = true;
final String enabledProperty =
this.service.getCustomProperties().get(CloudifyConstants.CUSTOM_PROPERTY_ENABLE_PID_MONITOR);
if (enabledProperty != null) {
enabled = Boolean.parseBoolean(enabledProperty);
}
if (enabled) {
return new ProcessStopDetector();
}
logger.warning("PID Based stop detection has been disabled due to custom property setting: "
+ CloudifyConstants.CUSTOM_PROPERTY_ENABLE_PID_MONITOR);
return null;
}
/*******
* Stop detection implementation that checks if the start command exited abnormally. This detector flags a service
* as stopped if the start command has exited with a non-zero exit code.
*
* @return {@link StopDetector} implementation
*/
@Bean
public USMEvent getStartProcessStopDetection() {
boolean enabled = true;
final String enabledProperty =
this.service.getCustomProperties().get(CloudifyConstants.CUSTOM_PROPERTY_ENABLE_START_PROCESS_MONITOR);
if (enabledProperty != null) {
enabled = Boolean.parseBoolean(enabledProperty);
}
if (!enabled) {
logger.warning("Monitoring of the start command process has been disabled due to custom property setting: "
+ CloudifyConstants.CUSTOM_PROPERTY_ENABLE_START_PROCESS_MONITOR);
return null;
}
return new StopDetector() {
private UniversalServiceManagerBean usm;
@Override
public void init(final UniversalServiceManagerBean usm) {
this.usm = usm;
}
@Override
public int getOrder() {
return 5;
}
@Override
public boolean isServiceStopped()
throws USMException {
logger.fine("Start Process Fail Detection - Running");
final Integer exitCode = USMUtils.getProcessExitCode(usm.getStartProcess());
// start process has not terminated, indicating a foreground process
if (exitCode == null) {
logger.fine("Start Process Fail Detection - Process is still running");
return false;
}
// start process has terminated.
if (exitCode == 0) {
logger.fine("Start Process Fail Detection - Process has stopped successfully");
// 0 exit code indicates start command finished successfully - must be a background process.
return false;
}
// start command exited with error
logger.severe("The start command has exited with the abnormal exit code: " + exitCode
+ ". Service will now stop!");
return true;
}
};
}
@Bean
public USMEvent getDSLStopDetection() {
final ExecutableDSLEntry detector = this.service.getLifecycle().getStopDetection();
if (detector == null) {
return null;
}
return new StopDetector() {
@Override
public void init(final UniversalServiceManagerBean usm) {
}
@Override
public int getOrder() {
return 5;
}
@Override
public boolean isServiceStopped()
throws USMException {
final EventResult result = new DSLEntryExecutor(detector, launcher, puExtDir, LifecycleEvents.STOP_DETECTION).run();
if (result.isSuccess()) {
final Object retcode = result.getResult();
if (retcode instanceof Boolean) {
return (Boolean) retcode;
} else {
throw new USMException("A stop detector returned a result that is not a boolean. Result was: "
+ retcode);
}
} else {
throw new USMException(
"A Stop Detector failed to execute. Exception was: " + result.getException(),
result.getException());
}
}
};
}
@Bean
public USMEvent getDSLStartDetection() {
final ExecutableDSLEntry detector = this.service.getLifecycle().getStartDetection();
if (detector == null) {
return null;
}
return new LivenessDetector() {
@Override
public boolean isProcessAlive()
throws USMException {
final EventResult result = new DSLEntryExecutor(detector, launcher, puExtDir, LifecycleEvents.START_DETECTION).run();
if (result.isSuccess()) {
final Object retcode = result.getResult();
if (retcode instanceof Boolean) {
return (Boolean) retcode;
}
// If closure, make sure return value is boolean.
if(detector instanceof ClosureExecutableEntry) {
logger.warning("A start detector closure return a result that is not boolean! Result was: " + retcode);
return false;
}
return true;
}
// process exited with abnormal status code
logger.log(Level.WARNING, "Liveness Detector failed to execute. Exception was: "
+ result.getException(), result.getException());
return false;
}
@Override
public void init(final UniversalServiceManagerBean usm) {
}
@Override
public int getOrder() {
return 5;
}
};
}
@Bean
public USMEvent getNetworkEventHandler() {
if (this.service.getNetwork() == null) {
return null;
}
// check for override for this setting
final String override =
this.service.getCustomProperties().get(CloudifyConstants.CUSTOM_PROPERTY_ENABLE_TCP_PORT_MONITOR);
if (override != null && override.equalsIgnoreCase("false")) {
return null;
}
final String protocol = this.service.getNetwork().getProtocolDescription();
final int port = this.service.getNetwork().getPort();
if (protocol == null) {
return new TCPPortEventListener(port);
} else if (protocol.equalsIgnoreCase("tcp") || protocol.equalsIgnoreCase("http")) {
if (port <= 0) {
return null;
}
return new TCPPortEventListener(port);
} else {
return null;
}
}
@Bean
public USMEvent getStop() {
final ExecutableDSLEntry stop = this.service.getLifecycle().getStop();
if (stop != null) {
// will be executed by the dsl command lifecycle listener.
return null;
} else {
return new DefaultStop();
}
}
@Bean
public ProcessLocator getDslLocator() {
final ExecutableDSLEntry locator = this.service.getLifecycle().getLocator();
if (locator != null) {
return new ProcessLocatorExecutor(locator, launcher, puExtDir);
} else {
return new DefaultProcessLocator();
}
}
@Override
public void setApplicationContext(final ApplicationContext applicationContext) {
this.context = (ResourceApplicationContext) applicationContext;
}
public ServiceConfiguration getConfiguration() {
return configuration;
}
public void setConfiguration(final ServiceConfiguration configuration) {
this.configuration = configuration;
}
// CHECKSTYLE:ON
}