changeset 7:93c6c1c31eb8

Load configured backends by classname.
author Jon VanAlten <jon.vanalten@redhat.com>
date Mon, 28 Nov 2011 17:57:18 -0500
parents 45e55e0d1e65
children 3db016a4e023
files config/agent.properties src/com/redhat/thermostat/agent/Agent.java src/com/redhat/thermostat/agent/Main.java src/com/redhat/thermostat/agent/config/Configuration.java src/com/redhat/thermostat/backend/Backend.java src/com/redhat/thermostat/backend/BackendLoadException.java src/com/redhat/thermostat/backend/BackendRegistry.java src/com/redhat/thermostat/backend/sample/SampleBackend.java src/com/redhat/thermostat/common/Constants.java src/com/redhat/thermostat/common/LaunchException.java
diffstat 10 files changed, 256 insertions(+), 139 deletions(-) [+]
line wrap: on
line diff
--- a/config/agent.properties	Wed Nov 23 17:18:53 2011 -0500
+++ b/config/agent.properties	Mon Nov 28 17:57:18 2011 -0500
@@ -8,11 +8,18 @@
 config_url=mongodb://127.0.0.1
 #
 ## Backend Configuration
-# This must be a comma separated list naming each backend that should run
+# This must be a comma separated list naming the fully qualified class name for
+# each backend that should run
 backends=
-# For each backend specified above, there must be a .active property specified.
+# Backends may also use their name as prefix for backend-specific configuration.
+# For example, if backend foo requires a property called bar, then a line
+# containing 'foo.bar=baz' should be included.  'foo' in this case should be
+# the human-friendly alias for the backend, ie the string returned by the
+# getName() method of the class listed in the backends string.
+#
+# For each backend, there may be a .active property specified.
 # ie: for backend 'foo' there should be a 'foo.active = bar' line.  'bar'
-# should be a comma separated list including one or more of:
+# must be a comma separated list including one or more of:
 #   'new' - The backend should attempt to attach to any new java process that
 #           starts.  Existing processes should not be instrumented.
 #   'all' - The backend should attempt to attach to all existing java
@@ -21,7 +28,11 @@
 #           are equivalent to the Linux process id.  This allows for the case
 #           of specific existing java processes to be instrumented.
 #
-# Backends may also use their name as prefix for backend-specific configuration.
-# For example, if backend foo requires a property called bar, then a line
-# containing 'foo.bar=baz' should be included.
-
+# Alternatively, you may specify 'none' and the backend will not begin collecting
+# from any VM, but will be available to clients who wish to activate it.
+#
+# If there is no .active property specified, then 'none' is implied.
+#
+## Sample backend configuration
+#sample-backend.active=none
+#sample-backend.myconfiguration=property
\ No newline at end of file
--- a/src/com/redhat/thermostat/agent/Agent.java	Wed Nov 23 17:18:53 2011 -0500
+++ b/src/com/redhat/thermostat/agent/Agent.java	Mon Nov 28 17:57:18 2011 -0500
@@ -1,11 +1,16 @@
 package com.redhat.thermostat.agent;
 
 import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import com.mongodb.DB;
 import com.redhat.thermostat.agent.config.Configuration;
+import com.redhat.thermostat.backend.Backend;
 import com.redhat.thermostat.backend.BackendRegistry;
 import com.redhat.thermostat.common.Constants;
+import com.redhat.thermostat.common.LaunchException;
+import com.redhat.thermostat.common.utils.LoggingUtils;
 
 /**
  * Represents the Agent running on a host.
@@ -16,6 +21,8 @@
     private final BackendRegistry backendRegistry;
     private final Configuration config;
 
+    private static final Logger LOGGER = LoggingUtils.getLogger(Agent.class);
+
     private DB database;
 
     public Agent(BackendRegistry backendRegistry, Configuration config, DB db) {
@@ -30,23 +37,33 @@
         config.setCollection(database.getCollection(Constants.AGENT_CONFIG_COLLECTION_NAME));
     }
 
-    private void loadConfiguredBackends() {
-        // TODO Once Configuration has relevant methods for getting list of backend names and backend-specific parameters, iterate over that list,
-        // activating as per configuration parameters and adding each to the registry
+    private void startBackends() throws LaunchException {
+        for (Backend be : backendRegistry.getAll()) {
+            if (!be.activate()) {
+                // When encountering issues during startup, we should not attempt to continue activating.
+                stopBackends();
+                throw new LaunchException("Could not activate backend: " + be.getName());
+            }
+        }
     }
 
-    private void stopAllBackends() {
-        // TODO Inverse of the above.  Stop each backend, remove from registry.
+    private void stopBackends() {
+        for (Backend be : backendRegistry.getAll()) {
+            if (!be.deactivate()) {
+                // When encountering issues during shutdown, we should attempt to shut down remaining backends.
+                LOGGER.log(Level.WARNING, "Issue while deactivating backend: " + be.getName());
+            }
+        }
     }
 
-    public void start() {
-        loadConfiguredBackends();
+    public void start() throws LaunchException {
+        startBackends();
         config.publish();
     }
 
     public void stop() {
         config.unpublish();
-        stopAllBackends();
+        stopBackends();
     }
 
     public UUID getId() {
--- a/src/com/redhat/thermostat/agent/Main.java	Wed Nov 23 17:18:53 2011 -0500
+++ b/src/com/redhat/thermostat/agent/Main.java	Mon Nov 28 17:57:18 2011 -0500
@@ -11,6 +11,7 @@
 import com.mongodb.Mongo;
 import com.mongodb.MongoURI;
 import com.redhat.thermostat.agent.config.Configuration;
+import com.redhat.thermostat.backend.BackendLoadException;
 import com.redhat.thermostat.backend.BackendRegistry;
 import com.redhat.thermostat.common.Constants;
 import com.redhat.thermostat.common.LaunchException;
@@ -52,7 +53,14 @@
 
         logger.setLevel(config.getLogLevel());
 
-        BackendRegistry backendRegistry = BackendRegistry.getInstance();
+        BackendRegistry backendRegistry = null;
+        try {
+            backendRegistry = new BackendRegistry(config);
+        } catch (BackendLoadException ble) {
+            System.err.println("Could not get BackendRegistry instance.");
+            ble.printStackTrace();
+            System.exit(Constants.EXIT_BACKEND_LOAD_ERROR);
+        }
 
         Mongo mongo = null;
         DB db = null;
@@ -69,7 +77,13 @@
 
         Agent agent = new Agent(backendRegistry, config, db);
         config.setAgent(agent);
-        agent.start();
+        try {
+            agent.start();
+        } catch (LaunchException le) {
+            System.err.println("Agent could not start, probably because a configured backend could not be activated.");
+            le.printStackTrace();
+            System.exit(Constants.EXIT_BACKEND_START_ERROR);
+        }
         logger.fine("agent published");
 
         try {
--- a/src/com/redhat/thermostat/agent/config/Configuration.java	Wed Nov 23 17:18:53 2011 -0500
+++ b/src/com/redhat/thermostat/agent/config/Configuration.java	Mon Nov 28 17:57:18 2011 -0500
@@ -2,14 +2,13 @@
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
-import java.util.Set;
 import java.util.logging.Level;
 
 import com.mongodb.BasicDBObject;
@@ -23,23 +22,32 @@
 
 public final class Configuration {
 
+    /* FIXME
+     * 
+     * This class needs some love.  It mixes up startup configuration with runtime configuration,
+     * while each is handled in very different ways.  It probably should be split into separate
+     * classes, but it makes very little sense to do that before we have a Storage abstraction
+     * hiding implementation details (ie Mongo API stuff).
+     */
+    private Properties props;
+
     private Level logLevel;
     private boolean localMode;
     private int mongodPort;
     private int mongosPort;
     private String databaseURI;
     private String completeDatabaseURI;
-    private Backends backends;
 
     private String hostname;
 
     private Agent agent;
-    private boolean published = false;
     private DBCollection dbCollection = null;
 
     public Configuration(String[] args, Properties props) throws LaunchException {
+        this.props = props;
+
         initFromDefaults();
-        initFromProperties(props);
+        initFromProperties();
         initFromArguments(args);
 
         if (localMode) {
@@ -64,14 +72,13 @@
         databaseURI = Defaults.DATABASE_URI;
     }
 
-    private void initFromProperties(Properties props) {
+    private void initFromProperties() {
         if (props.getProperty(Constants.AGENT_PROPERTY_MONGOD_PORT) != null) {
             mongodPort = Integer.valueOf(props.getProperty(Constants.AGENT_PROPERTY_MONGOD_PORT));
         }
         if (props.getProperty(Constants.AGENT_PROPERTY_MONGOS_PORT) != null) {
             mongosPort = Integer.valueOf(props.getProperty(Constants.AGENT_PROPERTY_MONGOS_PORT));
         }
-        backends = new Backends(props);
     }
 
     private void initFromArguments(String[] args) throws LaunchException {
@@ -86,6 +93,7 @@
         }
     }
 
+    // TODO hide Mongo stuff behind Storage facade
     public void setCollection(DBCollection collection) {
         dbCollection = collection;
     }
@@ -102,12 +110,13 @@
         return hostname;
     }
 
+    // TODO all of this should be assembled somewhere behind the Storage facade, once it exists.
     public DBObject toDBObject() {
         BasicDBObject result = new BasicDBObject();
         // TODO explicit exception if agent not yet set.
         result.put(Constants.AGENT_ID, agent.getId().toString());
         result.put(Constants.AGENT_CONFIG_KEY_HOST, hostname);
-        result.put(Constants.AGENT_CONFIG_KEY_BACKENDS, backends.toDBObject());
+        // TODO create nested backend config parts
         return result;
     }
 
@@ -116,29 +125,52 @@
     }
 
     public void publish() {
-        // TODO explicit exception if dbCollection has not yet been set.
+        // TODO Hide Mongo stuff behind Storage facade.
         dbCollection.insert(toDBObject(), WriteConcern.SAFE);
         // TODO Start configuration-change-detection thread.
-        published = true;
     }
 
     public void unpublish() {
         // TODO Stop configuration-change-detection thread.
+        // TODO hide Mongo stuff behind storage facade.
         dbCollection.remove(new BasicDBObject(Constants.AGENT_ID, agent.getId().toString()), WriteConcern.NORMAL);
-        published = false;
+    }
+
+    public List<String> getStartupBackendClassNames() {
+        String fullPropertyString = props.getProperty(Constants.AGENT_PROPERTY_BACKENDS);
+        if ((fullPropertyString == null) // Avoid NPE
+                || (fullPropertyString.length() == 0)) { /* If we do the split() on this empty string,
+                                                          * it will produce an array of size 1 containing
+                                                          * the empty string, which we do not want.
+                                                          */
+            return new ArrayList<String>();
+        } else {
+            return Arrays.asList(fullPropertyString.trim().split(","));
+        }
     }
 
-    public String getBackendConfigValue(String backendName, String configurationKey) {
-        String value = null;
-        if (published) {
-            value = getBackendConfigFromDatabase(backendName, configurationKey);
-        } else {
-            value = backends.getConfigValue(backendName, configurationKey);
+    public Map<String, String> getStartupBackendConfigMap(String backendName) {
+        String prefix = backendName + ".";
+        Map<String, String> configMap = new HashMap<String, String>();
+        for (Entry<Object, Object> e : props.entrySet()) {
+            String key = (String) e.getKey();
+            if (key.startsWith(prefix)) {
+                String value = (String) e.getValue();
+                String mapKey = key.substring(prefix.length());
+                configMap.put(mapKey, value);
+            }
         }
-        return value;
+        return configMap;
     }
 
-    private String getBackendConfigFromDatabase(String backendName, String configurationKey) {
+    /**
+     * 
+     * @param backendName
+     * @param configurationKey
+     * @return
+     */
+    public String getBackendConfigValue(String backendName, String configurationKey) {
+        // TODO hide Mongo stuff behind Storage facade.
         DBObject config = dbCollection.findOne(new BasicDBObject(Constants.AGENT_ID, agent.getId().toString()));
         // TODO get the appropriate value from this agent's configuration.
         return null;
@@ -210,74 +242,4 @@
             return logLevel;
         }
     }
-
-    /**
-     * A wrapper around the backend-specific information.  Used mainly for convenience during startup; once running all config should happen via database.
-     */
-    private static class Backends {
-        /* TODO Do we really need to do all this mapping?  These values should only be used at startup, maybe best to just have convenience wrappers
-         * around the properties file...
-         */
-        /** {backend-name: { opt1: va1, opt2:val2, } } */
-        private Map<String, Map<String, String>> info;
-
-        public Backends(Properties props) {
-            info = new HashMap<String, Map<String, String>>();
-            initializeFromProperties(props);
-        }
-
-        private void initializeFromProperties(Properties props) {
-            List<String> backendNames = Arrays.asList(props.getProperty(Constants.AGENT_PROPERTY_BACKENDS).trim().split(","));
-            for (String backendName : backendNames) {
-                // TODO Initialize Map<String, String> of properties for each.
-            }
-        }
-
-        public String getConfigValue(String backendName, String configurationKey) {
-            // TODO make this more robust, appropriate exceptions for invalid input values.
-            Map<String, String> backendValues = info.get(backendName);
-            return backendValues.get(configurationKey);
-        }
-
-        private void addBackend(String backendName) {
-            if (!info.containsKey(backendName)) {
-                info.put(backendName, new HashMap<String, String>());
-            }
-        }
-
-        public Object toDBObject() {
-            BasicDBObject result = new BasicDBObject();
-            for (Entry<String, Map<String, String>> e : info.entrySet()) {
-                BasicDBObject config = new BasicDBObject();
-                config.putAll(e.getValue());
-                result.put(e.getKey(), config);
-            }
-            return result;
-        }
-
-        public Set<String> getBackends() {
-            return info.keySet();
-        }
-
-        public Set<String> getMatchingBackends(String key, String value) {
-            // TODO perhaps extend this to regex?
-            Set<String> matched = new HashSet<String>();
-            for (Entry<String, Map<String, String>> e : info.entrySet()) {
-                if (e.getValue().get(key) != null && e.getValue().get(key).equals(value)) {
-                    matched.add(e.getKey());
-                }
-            }
-            return matched;
-        }
-
-        public void addConfig(String backend, String key, String value) {
-            addBackend(backend);
-            info.get(backend).put(key, value);
-        }
-
-        public Map<String, String> getConfig(String backend) {
-            return info.get(backend);
-        }
-
-    }
 }
--- a/src/com/redhat/thermostat/backend/Backend.java	Wed Nov 23 17:18:53 2011 -0500
+++ b/src/com/redhat/thermostat/backend/Backend.java	Mon Nov 28 17:57:18 2011 -0500
@@ -8,21 +8,20 @@
  * registered with the {@link BackendRegistry}.
  */
 public abstract class Backend {
-    private String name;
 
-    public Backend(String name, Map<String, String> properties) {
-        this.name = name;
-        for (Entry<String, String> e : properties.entrySet()) {
+    public Backend() {
+    }
+
+    public final void setInitialConfiguration(Map<String, String> configMap) {
+        for (Entry<String, String> e : configMap.entrySet()) {
             setConfigurationValue(e.getKey(), e.getValue());
         }
     }
 
-    public abstract void setConfigurationValue(String name, String value);
+    protected abstract void setConfigurationValue(String name, String value);
 
     /** Returns the name of the {@link Backend} */
-    public String getName() {
-        return name;
-    }
+    public abstract String getName();
 
     /** Returns the description of the {@link Backend} */
     public abstract String getDescription();
@@ -39,16 +38,19 @@
     public abstract Map<String, String> getConfigurationMap();
 
     /**
-     * Activate the {@link Backend}. The backend should start gathering general
-     * data (not specific to any vm) and start sending it
+     * Activate the {@link Backend}.  Based on the current configuration,
+     * begin pushing data to the Storage layer.  If the {@link Backend} is
+     * already active, this method should have no effect
      *
      * @return true on success, false if there was an error
      */
     public abstract boolean activate();
 
     /**
-     * Deactivate the {@link Backend}. The backend should release resources at
-     * this point.
+     * Deactivate the {@link Backend}. The backend should release any
+     * resources that were obtained as a direct result of a call to
+     * {@link #activate()}.  If the {@link Backend} is not active, this
+     * method should have no effect
      *
      * @return true on success
      */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/com/redhat/thermostat/backend/BackendLoadException.java	Mon Nov 28 17:57:18 2011 -0500
@@ -0,0 +1,14 @@
+package com.redhat.thermostat.backend;
+
+public class BackendLoadException extends Exception {
+
+    private static final long serialVersionUID = 4057881401012295723L;
+
+    public BackendLoadException(String message) {
+        super(message);
+    }
+
+    public BackendLoadException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
--- a/src/com/redhat/thermostat/backend/BackendRegistry.java	Wed Nov 23 17:18:53 2011 -0500
+++ b/src/com/redhat/thermostat/backend/BackendRegistry.java	Mon Nov 28 17:57:18 2011 -0500
@@ -1,7 +1,14 @@
 package com.redhat.thermostat.backend;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.lang.reflect.Constructor;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.redhat.thermostat.agent.config.Configuration;
+import com.redhat.thermostat.common.utils.LoggingUtils;
 
 /**
  * A registry for {@link Backend}s. Each {@link Backend} should call
@@ -9,35 +16,46 @@
  */
 public final class BackendRegistry {
 
-    private static BackendRegistry INSTANCE = null;
+    private static final Logger LOGGER = LoggingUtils.getLogger(BackendRegistry.class);
+
+    private final Map<String, Backend> registeredBackends;
 
-    private final List<Backend> registeredBackends;
-
-    private BackendRegistry() {
-        registeredBackends = new ArrayList<Backend>();
+    public BackendRegistry(Configuration config) throws BackendLoadException {
+        registeredBackends = new HashMap<String, Backend>();
+        
+        for (String backendClassName : config.getStartupBackendClassNames()) {
+            LOGGER.log(Level.FINE, "Initializing backend: \"" + backendClassName + "\"");
+            Backend backend = null;
+            try {
+                Class<? > c = Class.forName(backendClassName);
+                Class<? extends Backend> narrowed = c.asSubclass(Backend.class);
+                Constructor<? extends Backend> backendConstructor = narrowed.getConstructor();
+                backend = backendConstructor.newInstance();
+                backend.setInitialConfiguration(config.getStartupBackendConfigMap(backend.getName()));
+            } catch (Exception e) {
+                throw new BackendLoadException("Could not instantiate configured backend class: " + backendClassName, e);
+            }
+            register(backend);
+        }
     }
 
-    public static synchronized BackendRegistry getInstance() {
-        if (INSTANCE == null) {
-            INSTANCE = new BackendRegistry();
+    private synchronized void register(Backend backend) throws BackendLoadException {
+        if (registeredBackends.containsKey(backend.getName())) {
+            throw new BackendLoadException("Attempt to register two backends with the same name: " + backend.getName());
         }
-        return INSTANCE;
+        registeredBackends.put(backend.getName(), backend);
     }
 
-    public synchronized void register(Backend backend) {
-        registeredBackends.add(backend);
+    private synchronized void unregister(Backend backend) {
+        registeredBackends.remove(backend.getName());
     }
 
-    public synchronized void unregister(Backend backend) {
-        registeredBackends.remove(backend);
-    }
-
-    public synchronized Backend[] getAll() {
-        return registeredBackends.toArray(new Backend[0]);
+    public synchronized Collection<Backend> getAll() {
+        return registeredBackends.values();
     }
 
     public synchronized Backend getByName(String name) {
-        for (Backend backend : registeredBackends) {
+        for (Backend backend : registeredBackends.values()) {
             if (backend.getName().equals((name))) {
                 return backend;
             }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/com/redhat/thermostat/backend/sample/SampleBackend.java	Mon Nov 28 17:57:18 2011 -0500
@@ -0,0 +1,75 @@
+package com.redhat.thermostat.backend.sample;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.redhat.thermostat.backend.Backend;
+import com.redhat.thermostat.common.utils.LoggingUtils;
+
+/** Just an example backend implementation.  This is really just to test the loading and configuration mechanisms
+ *
+ */
+public class SampleBackend extends Backend {
+    private final String NAME = "sample-backend";
+    private final String DESCRIPTION = "A backend which does nothing at all.";
+    private final String VENDOR = "Nobody";
+    private final String VERSION = "0.1";
+    private boolean currentlyActive = false;
+
+    private Logger LOGGER = LoggingUtils.getLogger(SampleBackend.class);
+
+    public SampleBackend() {
+        super();
+    }
+
+    @Override
+    protected void setConfigurationValue(String name, String value) {
+        LOGGER.log(Level.FINE, "Setting configuration value for backend: " + this.NAME);
+        LOGGER.log(Level.FINE, "key: " + name + "    value: " + value);
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public String getDescription() {
+        return DESCRIPTION;
+    }
+
+    @Override
+    public String getVendor() {
+        return VENDOR;
+    }
+
+    @Override
+    public String getVersion() {
+        return VERSION;
+    }
+
+    @Override
+    public Map<String, String> getConfigurationMap() {
+        return new HashMap<String, String>();
+    }
+
+    @Override
+    public boolean activate() {
+        currentlyActive = true;
+        return true;
+    }
+
+    @Override
+    public boolean deactivate() {
+        currentlyActive = false;
+        return true;
+    }
+
+    @Override
+    public boolean isActive() {
+        return currentlyActive;
+    }
+
+}
--- a/src/com/redhat/thermostat/common/Constants.java	Wed Nov 23 17:18:53 2011 -0500
+++ b/src/com/redhat/thermostat/common/Constants.java	Mon Nov 28 17:57:18 2011 -0500
@@ -14,6 +14,8 @@
     public static final int EXIT_UNABLE_TO_CONNECT_TO_DATABASE = 2;
     public static final int EXIT_UNABLE_TO_READ_PROPERTIES = 3;
     public static final int EXIT_CONFIGURATION_ERROR = 4;
+    public static final int EXIT_BACKEND_LOAD_ERROR = 5;
+    public static final int EXIT_BACKEND_START_ERROR = 6;
 
     public static final String THERMOSTAT_DB = "thermostat";
 
@@ -34,4 +36,5 @@
     public static final String AGENT_PROPERTY_BACKENDS = "backends";
 
     public static final String AGENT_LOCAL_HOSTNAME = "localhost";
+
 }
--- a/src/com/redhat/thermostat/common/LaunchException.java	Wed Nov 23 17:18:53 2011 -0500
+++ b/src/com/redhat/thermostat/common/LaunchException.java	Mon Nov 28 17:57:18 2011 -0500
@@ -4,9 +4,10 @@
  * of program.
  *
  */
-@SuppressWarnings("serial")
 public class LaunchException extends Exception {
 
+    private static final long serialVersionUID = -6757521147558143649L;
+
     public LaunchException(String message) {
         super(message);
     }