/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.foundationdb.server.service.servicemanager;
import com.foundationdb.sql.LayerInfoInterface;
import com.foundationdb.server.error.ServiceStartupException;
import com.foundationdb.server.service.Service;
import com.foundationdb.server.service.ServiceManager;
import com.foundationdb.server.service.config.ConfigurationService;
import com.foundationdb.server.service.dxl.DXLService;
import com.foundationdb.server.service.monitor.MonitorService;
import com.foundationdb.server.service.jmx.JmxManageable;
import com.foundationdb.server.service.jmx.JmxRegistryService;
import com.foundationdb.server.service.plugins.Plugin;
import com.foundationdb.server.service.plugins.PluginsFinder;
import com.foundationdb.server.service.servicemanager.configuration.BindingsConfigurationLoader;
import com.foundationdb.server.service.servicemanager.configuration.DefaultServiceConfigurationHandler;
import com.foundationdb.server.service.servicemanager.configuration.ServiceBinding;
import com.foundationdb.server.service.servicemanager.configuration.ServiceConfigurationHandler;
import com.foundationdb.server.service.servicemanager.configuration.yaml.YamlConfiguration;
import com.foundationdb.server.service.session.SessionService;
import com.foundationdb.server.service.stats.StatisticsService;
import com.foundationdb.server.store.SchemaManager;
import com.foundationdb.server.store.Store;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public final class GuicedServiceManager implements ServiceManager, JmxManageable {
// ServiceManager interface
@Override
public State getState() {
return state;
}
@Override
public synchronized void startServices() {
logger.info("Starting services.");
state = State.STARTING;
getJmxRegistryService().register(this);
boolean ok = false;
try {
for (Class<?> directlyRequiredClass : guicer.directlyRequiredClasses()) {
guicer.get(directlyRequiredClass, STANDARD_SERVICE_ACTIONS);
}
ok = true;
}
finally {
if (!ok)
state = State.ERROR_STARTING;
}
state = State.ACTIVE;
LayerInfoInterface layerInfo = getLayerInfo();
logger.info("{} {} ready.", layerInfo.getServerName(), layerInfo.getVersionInfo().versionLong);
}
@Override
public synchronized void stopServices() throws Exception {
logger.info("Stopping services normally.");
state = State.STOPPING;
try {
guicer.stopAllServices(STANDARD_SERVICE_ACTIONS);
}
finally {
state = State.IDLE;
}
logger.info("Services stopped.");
}
@Override
public synchronized void crashServices() throws Exception {
logger.info("Stopping services abnormally.");
state = State.STOPPING;
try {
guicer.stopAllServices(CRASH_SERVICES);
}
finally {
state = State.IDLE;
}
logger.info("Services stopped.");
}
@Override
public ConfigurationService getConfigurationService() {
return getServiceByClass(ConfigurationService.class);
}
@Override
public LayerInfoInterface getLayerInfo() {
return getServiceByClass(LayerInfoInterface.class);
}
@Override
public Store getStore() {
return getServiceByClass(Store.class);
}
@Override
public SchemaManager getSchemaManager() {
return getServiceByClass(SchemaManager.class);
}
@Override
public JmxRegistryService getJmxRegistryService() {
return getServiceByClass(JmxRegistryService.class);
}
@Override
public StatisticsService getStatisticsService() {
return getServiceByClass(StatisticsService.class);
}
@Override
public SessionService getSessionService() {
return getServiceByClass(SessionService.class);
}
@Override
public synchronized <T> T getServiceByClass(Class<T> serviceClass) {
return guicer.get(serviceClass, STANDARD_SERVICE_ACTIONS);
}
@Override
public DXLService getDXL() {
return getServiceByClass(DXLService.class);
}
@Override
public MonitorService getMonitorService() {
return getServiceByClass(MonitorService.class);
}
@Override
public boolean serviceIsBoundTo(Class<?> serviceClass, Class<?> implClass) {
return guicer.isBoundTo(serviceClass, implClass);
}
@Override
public boolean serviceIsStarted(Class<?> serviceClass) {
return guicer.serviceIsStarted(serviceClass);
}
// JmxManageable interface
@Override
public JmxObjectInfo getJmxObjectInfo() {
return new JmxObjectInfo("Services", bean, ServiceManagerMXBean.class);
}
// GuicedServiceManager interface
public GuicedServiceManager() {
this(standardUrls());
}
public GuicedServiceManager(BindingsConfigurationProvider bindingsConfigurationProvider) {
DefaultServiceConfigurationHandler configurationHandler = new DefaultServiceConfigurationHandler();
// Install the default, no-op JMX registry; this is a special case, since we want to use it
// as we start each service.
configurationHandler.bind(JmxRegistryService.class.getName(), NoOpJmxRegistry.class.getName(), null);
// Next, load each element in the provider...
for (BindingsConfigurationLoader loader : bindingsConfigurationProvider.loaders()) {
loader.loadInto(configurationHandler);
}
// ... followed by the configured or default file, if either exists
URL configFile = findServiceConfigFile();
if (configFile != null) {
new YamlBindingsUrl(configFile).loadInto(configurationHandler);
}
// ... followed by any command-line overrides.
new PropertyBindings(System.getProperties()).loadInto(configurationHandler);
Collection<ServiceBinding> bindings = configurationHandler.serviceBindings(false);
BindingsConfigurationLoader pluginsConfigLoader = getPluginsConfigurationLoader(bindings);
pluginsConfigLoader.loadInto(configurationHandler);
bindings = configurationHandler.serviceBindings(true);
try {
guicer = Guicer.forServices(ServiceManager.class, this,
bindings, configurationHandler.priorities(), configurationHandler.getModules());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
// private methods
private BindingsConfigurationLoader getPluginsConfigurationLoader(Collection<ServiceBinding> bindings) {
ServiceBinding pluginsFinderBinding = null;
for (ServiceBinding binding : bindings) {
if (PluginsFinder.class.getCanonicalName().equals(binding.getInterfaceName())) {
if (pluginsFinderBinding != null)
throw new ServiceStartupException("multiple bindings found for " + PluginsFinder.class);
pluginsFinderBinding = binding;
}
}
if (pluginsFinderBinding == null)
return emptyConfigurationLoader;
String pluginsFinderClassName = pluginsFinderBinding.getImplementingClassName();
Class<?> pluginsFinderClass;
try {
pluginsFinderClass = Class.forName(pluginsFinderClassName);
}
catch (ClassNotFoundException e) {
throw new ServiceStartupException("couldn't get Class object for " + pluginsFinderClassName);
}
PluginsFinder pluginsFinder;
try {
pluginsFinder = (PluginsFinder) pluginsFinderClass.newInstance();
}
catch (Exception e) {
logger.error("while instantiating plugins finder", e);
logger.error("plugins finder must have a no-arg constructor, though there may be something else wrong");
throw new ServiceStartupException("error while instantiating plugins finder. please check logs");
}
CompositeConfigurationLoader compositeLoader = new CompositeConfigurationLoader();
Collection<? extends Plugin> plugins = pluginsFinder.get();
List<URL> pluginUrls = new ArrayList<>(plugins.size());
for (Plugin plugin : plugins) {
URL url = plugin.getClassLoaderURL();
if (url != null) {
pluginUrls.add(url);
}
}
ClassLoader pluginsClassloader = null;
if (!pluginUrls.isEmpty()) {
pluginsClassloader = new URLClassLoader(pluginUrls.toArray(new URL[pluginUrls.size()]));
}
for (Plugin plugin : plugins) {
try {
YamlConfiguration pluginConfig = new YamlConfiguration(
plugin.toString(),
plugin.getServiceConfigsReader(),
pluginsClassloader);
compositeLoader.add(pluginConfig);
}
catch (IOException e) {
logger.error("while reading services config for " + plugin, e);
throw new ServiceStartupException("error while reading services config for " + plugin);
}
}
return compositeLoader;
}
boolean isRequired(Class<?> theClass) {
return guicer.isRequired(theClass);
}
// static methods
public static BindingsConfigurationProvider standardUrls() {
BindingsConfigurationProvider provider = new BindingsConfigurationProvider();
provider.define(GuicedServiceManager.class.getResource("default-services.yaml"));
return provider;
}
public static BindingsConfigurationProvider testUrls() {
BindingsConfigurationProvider provider = standardUrls();
provider.define(GuicedServiceManager.class.getResource("test-services.yaml"));
provider.overrideRequires(GuicedServiceManager.class.getResource("test-services-requires.yaml"));
return provider;
}
/**
* <ul>
* <li>
* if {@link #SERVICES_CONFIG_PROPERTY} is defined,
* return {@link URL}
* </li>
* <li>
* if {@link #CONFIG_DIR_PROPERTY} is defined and
* <ul>
* <li>contains {@link #DEFAULT_CONFIG_FILE_NAME}, return {@link File}</li>
* </ul>
* </li>
* <li>
* return <code>null</code>
* </li>
* </ul>
*/
private static URL findServiceConfigFile() {
try {
String servicesConfigFile = System.getProperty(SERVICES_CONFIG_PROPERTY);
if(servicesConfigFile != null) {
return new URL(new File(".").toURI().toURL(), // Default to local file.
servicesConfigFile);
}
String configDir = System.getProperty(CONFIG_DIR_PROPERTY);
if(configDir != null) {
File configFile = new File(configDir, DEFAULT_CONFIG_FILE_NAME);
if(configFile.isFile()) {
return configFile.toURI().toURL();
}
}
} catch (MalformedURLException e) {
throw new RuntimeException("couldn't convert config file to URL", e);
}
return null;
}
// object state
private State state = State.IDLE;
private final Guicer guicer;
private final ServiceManagerMXBean bean = new ServiceManagerMXBean() {
@Override
public List<String> getStartedDependencies() {
boolean fullNames = isFullClassNames();
List<String> result = new ArrayList<>();
for (Class<?> requiredClass : guicer.directlyRequiredClasses()) {
List<?> dependencies = guicer.dependenciesFor(requiredClass);
List<String> dependenciesClasses = new ArrayList<>();
for (Object dependency : dependencies) {
Class<?> depClass = dependency.getClass();
dependenciesClasses.add(fullNames ? depClass.getName() : depClass.getSimpleName());
}
result.add(dependenciesClasses.toString());
}
return result;
}
@Override
public boolean isFullClassNames() {
return fullClassNames.get();
}
@Override
public void setFullClassNames(boolean value) {
fullClassNames.set(value);
}
@Override
public List<String> getServicesInStartupOrder() {
List<String> result = new ArrayList<>();
for (Class<?> serviceClass : guicer.servicesClassesInStartupOrder()) {
result.add(isFullClassNames() ? serviceClass.getName() : serviceClass.getSimpleName() );
}
return result;
}
private final AtomicBoolean fullClassNames = new AtomicBoolean(false);
};
final Guicer.ServiceLifecycleActions<Service> STANDARD_SERVICE_ACTIONS
= new Guicer.ServiceLifecycleActions<Service>()
{
private Map<Class<? extends JmxManageable>,ObjectName> jmxNames
= Collections.synchronizedMap(new HashMap<Class<? extends JmxManageable>, ObjectName>());
@Override
public void onStart(Service service) {
final Thread currentThread = Thread.currentThread();
final ClassLoader oldContextCl = currentThread.getContextClassLoader();
ClassLoader contextClassloader = service.getClass().getClassLoader();
boolean setContextCl = (contextClassloader != null && contextClassloader != oldContextCl);
try {
if (setContextCl)
currentThread.setContextClassLoader(contextClassloader);
service.start();
if (service instanceof JmxManageable && isRequired(JmxRegistryService.class)) {
JmxRegistryService registry = (service instanceof JmxRegistryService)
? (JmxRegistryService) service
: getJmxRegistryService();
JmxManageable manageable = (JmxManageable)service;
ObjectName objectName = registry.register(manageable);
jmxNames.put(manageable.getClass(), objectName);
// TODO because our dependency graph is created via Service.start() invocations, if service A uses service B
// in stop() but not start(), and service B has already been shut down, service B will be resurrected. Yuck.
// I don't know of a good way around this, other than by formalizing our dependency graph via constructor
// params (and thus removing ServiceManagerImpl.get() ). Until this is resolved, simplest is to just shrug
// our shoulders and not check
// assert (ObjectName)old == null : objectName + " has displaced " + old;
}
}
finally {
if (setContextCl)
currentThread.setContextClassLoader(oldContextCl);
}
}
@Override
public void onShutdown(Service service) {
if (service instanceof JmxManageable && isRequired(JmxRegistryService.class)) {
JmxRegistryService registry = (service instanceof JmxRegistryService)
? (JmxRegistryService) service
: getJmxRegistryService();
JmxManageable manageable = (JmxManageable) service;
ObjectName objectName = jmxNames.get(manageable.getClass());
if (objectName == null) {
throw new NullPointerException("service not registered: " + manageable.getClass());
}
registry.unregister(objectName);
}
service.stop();
}
@Override
public Service castIfActionable(Object object) {
return (object instanceof Service) ? (Service)object : null;
}
};
// consts
private static final String CONFIG_DIR_PROPERTY = "fdbsql.config_dir"; // Note: ConfigurationServiceImpl
private static final String DEFAULT_CONFIG_FILE_NAME = "services-config.yaml";
private static final String SERVICES_CONFIG_PROPERTY = "services.config";
private static final Guicer.ServiceLifecycleActions<Service> CRASH_SERVICES
= new Guicer.ServiceLifecycleActions<Service>() {
@Override
public void onStart(Service service) {
throw new UnsupportedOperationException();
}
@Override
public void onShutdown(Service service){
service.crash();
}
@Override
public Service castIfActionable(Object object) {
return (object instanceof Service) ? (Service) object : null;
}
};
private static final Logger LOG = LoggerFactory.getLogger(GuicedServiceManager.class);
// nested classes
/**
* Definition of URLs to use for defining service bindings. There are two sections of URls: the defines
* and requires. You can have as many defines as you want, but only one requires. When parsing the resources,
* the defines will be processed (in order) before the requires resource.
*/
public static final class BindingsConfigurationProvider {
// BindingsConfigurationProvider interface
/**
* Adds a URL to the the internal list.
* @param url the url to add
* @return this instance; useful for chaining
*/
public BindingsConfigurationProvider define(URL url) {
elements.add(new YamlBindingsUrl(url));
return this;
}
/**
* Adds a service binding to the internal list. This is equivalent to a yaml segment of
* {@code bind: {theInteface : theImplementation}}. For instance, it does not affect locking, and if the
* interface is locked, this will fail at run time.
* @param anInterface the interface to bind to
* @param anImplementation the implementing class
* @param <T> the interface's type
* @return this instance; useful for chaining
*/
public <T> BindingsConfigurationProvider bind(Class<T> anInterface, Class<? extends T> anImplementation) {
elements.add(new ManualServiceBinding(anInterface.getName(), anImplementation.getName(), false));
return this;
}
/**
* Adds a service binding to the internal list. This is equivalent to a yaml segment of
* {@code bind: {theInteface : theImplementation}}. For instance, it does not affect locking, and if the
* interface is locked, this will fail at run time.
* @param anInterface the interface to bind to
* @param anImplementation the implementing class
* @param <T> the interface's type
* @return this instance; useful for chaining
*/
public <T> BindingsConfigurationProvider bindAndRequire(Class<T> anInterface, Class<? extends T> anImplementation) {
elements.add(new ManualServiceBinding(anInterface.getName(), anImplementation.getName(), true));
return this;
}
/**
* Overrides the "requires" section of the URL definitions. This replaces the old requires URL.
* @param url the new requires URL
* @return this instance; useful for chaining
*/
public BindingsConfigurationProvider overrideRequires(URL url) {
requires = url;
return this;
}
// for use in this package
public Collection<BindingsConfigurationLoader> loaders() {
List<BindingsConfigurationLoader> urls = new ArrayList<>(elements);
if (requires != null) {
urls.add(new YamlBindingsUrl(requires));
}
return urls;
}
// object state
private final List<BindingsConfigurationLoader> elements = new ArrayList<>();
private URL requires = null;
}
private static class YamlBindingsUrl implements BindingsConfigurationLoader {
@Override
public void loadInto(ServiceConfigurationHandler config) {
final InputStream defaultServicesStream;
try {
defaultServicesStream = url.openStream();
} catch(IOException e) {
throw new RuntimeException("no resource " + url, e);
}
final Reader defaultServicesReader;
try {
defaultServicesReader = new InputStreamReader(defaultServicesStream, "UTF-8");
} catch (Exception e) {
try {
defaultServicesStream.close();
} catch (IOException ioe) {
LOG.error("while closing stream error", ioe);
}
throw new RuntimeException("while opening default services reader", e);
}
RuntimeException exception = null;
try {
new YamlConfiguration(url.toString(), defaultServicesReader, null).loadInto(config);
} catch (RuntimeException e) {
exception = e;
} finally {
try {
defaultServicesReader.close();
} catch (IOException e) {
if (exception == null) {
exception = new RuntimeException("while closing " + url, e);
}
else {
LOG.error("while closing url after exception " + exception, e);
}
}
}
if (exception != null) {
throw exception;
}
}
private YamlBindingsUrl(URL url) {
this.url = url;
}
private final URL url;
}
private static final BindingsConfigurationLoader emptyConfigurationLoader = new BindingsConfigurationLoader() {
@Override
public void loadInto(ServiceConfigurationHandler config) {}
};
private static class CompositeConfigurationLoader implements BindingsConfigurationLoader {
public void add(BindingsConfigurationLoader loader) {
loaders.add(loader);
}
@Override
public void loadInto(ServiceConfigurationHandler config) {
for (BindingsConfigurationLoader loader : loaders)
loader.loadInto(config);
}
private final List<BindingsConfigurationLoader> loaders = new ArrayList<>();
}
private static class ManualServiceBinding implements BindingsConfigurationLoader {
// BindingsConfigurationElement interface
@Override
public void loadInto(ServiceConfigurationHandler config) {
config.bind(interfaceName, implementationName, null);
if (required)
config.require(interfaceName);
}
// ManualServiceBinding interface
private ManualServiceBinding(String interfaceName, String implementationName, boolean required) {
this.interfaceName = interfaceName;
this.implementationName = implementationName;
this.required = required;
}
// object state
private final String interfaceName;
private final String implementationName;
private final boolean required;
}
static class PropertyBindings implements BindingsConfigurationLoader {
// BindingsConfigurationElement interface
@Override
public void loadInto(ServiceConfigurationHandler config) {
for (String property : properties.stringPropertyNames()) {
if (property.startsWith(BIND)) {
String theInterface = property.substring(BIND.length());
String theImpl = properties.getProperty(property);
if (theInterface.length() == 0) {
throw new IllegalArgumentException("empty -Dbind: property found");
}
if (theImpl.length() == 0) {
throw new IllegalArgumentException("-D" + property + " doesn't have a valid value");
}
config.bind(theInterface, theImpl, null);
} else if (property.startsWith(REQUIRE)) {
String theInterface = property.substring(REQUIRE.length());
String value = properties.getProperty(property);
if (value.length() != 0) {
throw new IllegalArgumentException(
String.format("-Drequire tags may not have values: %s = %s", theInterface, value)
);
}
config.require(theInterface);
} else if (property.startsWith(PRIORITIZE)) {
String theInterface = property.substring(PRIORITIZE.length());
String value = properties.getProperty(property);
if (value.length() != 0) {
throw new IllegalArgumentException(
String.format("-Dprioritize tags may not have values: %s = %s", theInterface, value)
);
}
config.prioritize(theInterface);
}
}
}
// PropertyBindings interface
PropertyBindings(Properties properties) {
this.properties = properties;
}
// for use in unit tests
// object state
private final Properties properties;
// consts
private static final String BIND = "bind:";
private static final String REQUIRE = "require:";
private static final String PRIORITIZE = "prioritize:";
}
public static class NoOpJmxRegistry implements JmxRegistryService {
@Override
public ObjectName register(JmxManageable service) {
try {
return new ObjectName("com.foundationdb:type=DummyPlaceholder" + counter.incrementAndGet());
} catch (MalformedObjectNameException e) {
throw new RuntimeException(e);
}
}
@Override
public void unregister(ObjectName registeredObject) {
}
@Override
public void unregister(String serviceName) {
}
// object state
private final AtomicInteger counter = new AtomicInteger();
}
}