dk.netarkivet.common.utils.JMXUtils.java Source code

Java tutorial

Introduction

Here is the source code for dk.netarkivet.common.utils.JMXUtils.java

Source

/* File:        $Id$
 * Revision:    $Revision$
 * Author:      $Author$
 * Date:        $Date$
 *
 * The Netarchive Suite - Software to harvest and preserve websites
 * Copyright 2004-2012 The Royal Danish Library, the Danish State and
 * University Library, the National Library of France and the Austrian
 * National Library.
 *
 * This library 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 library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package dk.netarkivet.common.utils;

import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.naming.ServiceUnavailableException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import com.sun.jndi.rmi.registry.RegistryContextFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import dk.netarkivet.common.CommonSettings;
import dk.netarkivet.common.exceptions.ArgumentNotValid;
import dk.netarkivet.common.exceptions.IOFailure;
import dk.netarkivet.common.exceptions.UnknownID;

/**
 * Various JMX-related utility functions.
 */
public final class JMXUtils {

    /** Private constructor to prevent instantiation. */
    private JMXUtils() {
    }

    /** The logger. */
    public static final Log log = LogFactory.getLog(JMXUtils.class.getName());

    /** The system property that Java uses to get an initial context
     * for JNDI. This must be set for RMI connections to work.
     */
    private static final String JNDI_INITIAL_CONTEXT_PROPERTY = "java.naming.factory.initial";
    /** seconds per milliseconds as a double figure. */
    private static final double DOUBLE_SECONDS_IN_MILLIS = TimeUtils.SECOND_IN_MILLIS * 1.0;
    /** The JMX timeout in seconds. */
    private static final long timeoutInseconds = Settings.getLong(CommonSettings.JMX_TIMEOUT);

    /** The maximum number of times we back off on getting an mbean or a job.
     * The cumulative time trying is 2^(MAX_TRIES) milliseconds,
     * thus the constant is defined as log_2(TIMEOUT), as set in settings.
     * @return The number of tries
     */
    public static int getMaxTries() {
        return (int) Math.ceil(Math.log((double) timeoutInseconds * DOUBLE_SECONDS_IN_MILLIS) / Math.log(2.0));
    }

    /**
     * @return the JMX timeout in milliseconds.
     */
    public static long getJmxTimeout() {
        return TimeUtils.SECOND_IN_MILLIS * timeoutInseconds;
    }

    /**
     * If no initial JNDI context has been configured,
     * configures the system to use Sun's standard one.
     * This is necessary for RMI connections to work.
     */
    private static void ensureJndiInitialContext() {

        if (System.getProperty(JNDI_INITIAL_CONTEXT_PROPERTY) == null) {
            System.setProperty(JNDI_INITIAL_CONTEXT_PROPERTY, RegistryContextFactory.class.getCanonicalName());
            log.info("Set property '" + JNDI_INITIAL_CONTEXT_PROPERTY + "' to: "
                    + RegistryContextFactory.class.getCanonicalName());
        } else {
            log.debug("Property '" + JNDI_INITIAL_CONTEXT_PROPERTY + "' is set to: "
                    + System.getProperty(JNDI_INITIAL_CONTEXT_PROPERTY));
        }
    }

    /**
     * Constructs the same service URL that JConsole does
     * on the basis of a server name, a JMX port number,
     * and a RMI port number.
     *
     * Example URL:
     * service:jmx:rmi://0.0.0.0:9999/jndi/rmi://0.0.0.0:1099/JMXConnector
     * where RMI port number = 9999, JMX port number = 1099
     * server = 0.0.0.0 a.k.a localhost(?).
     *
     * @param server The server that should be connected to using
     * the constructed URL.
     * @param jmxPort The number of the JMX port that should be connected to
     * using the constructed URL (may not be a negative number)
     * @param rmiPort The number of the RMI port that should be connected to
     * using the constructed URL, or -1 if the default RMI port should be used.
     * @return the constructed URL.
     */
    public static JMXServiceURL getUrl(String server, int jmxPort, int rmiPort) {
        ArgumentNotValid.checkNotNullOrEmpty(server, "String server");
        ArgumentNotValid.checkNotNegative(jmxPort, "int jmxPort");

        String url;
        if (rmiPort != -1) {
            url = "service:jmx:rmi://" + server + ":" + rmiPort + "/jndi/rmi://" + server + ":" + jmxPort
                    + "/jmxrmi";
        } else {
            url = "service:jmx:rmi:///jndi/rmi://" + server + ":" + jmxPort + "/jmxrmi";
        }
        log.trace("Created url for JMX-connections: " + url);
        try {
            return new JMXServiceURL(url);
        } catch (MalformedURLException e) {
            throw new UnknownID("Could not create new JMXServiceURL from " + url, e);
        }
    }

    /**
     * Returns a connection to a remote MbeanServer defined by
     * the given arguments.
     * @param server the remote servername
     * @param jmxPort the remote jmx-port
     * @param rmiPort the remote rmi-port
     * @param userName the username
     * @param password the password
     * @return a MBeanServerConnection to a remote MbeanServer defined by
     * the given arguments.
     */
    public static MBeanServerConnection getMBeanServerConnection(String server, int jmxPort, int rmiPort,
            String userName, String password) {
        ArgumentNotValid.checkNotNullOrEmpty(server, "String server");
        ArgumentNotValid.checkNotNegative(jmxPort, "int jmxPort");
        ArgumentNotValid.checkNotNegative(rmiPort, "int rmiPort");
        ArgumentNotValid.checkNotNullOrEmpty(userName, "String userName");
        ArgumentNotValid.checkNotNullOrEmpty(password, "String password");
        String logMsgSuffix = "a connection to server '" + server + "' on jmxport/rmiport=" + jmxPort + "/"
                + rmiPort + " using username=" + userName;
        log.debug("Establishing " + logMsgSuffix);
        JMXServiceURL jmxServiceUrl = getUrl(server, jmxPort, rmiPort);
        Map<String, String[]> credentials = packageCredentials(userName, password);
        MBeanServerConnection connection = getMBeanServerConnection(jmxServiceUrl, credentials);
        log.debug("Established successfully " + logMsgSuffix);
        return connection;
    }

    /**
     * Connects to the given (url-specified) service point,
     * sending the given credentials as login.
     * @param url The JMX service url of some JVM on some machine.
     * @param credentials a map with (at least) one entry, mapping
     * "jmx.remote.credentials" to a String array of length 2.
     * Its first item should be the user name.
     * Its second item should be the password.
     * @return An MBeanServerConnection representing the
     * connected session.
     */
    public static MBeanServerConnection getMBeanServerConnection(JMXServiceURL url,
            Map<String, String[]> credentials) {
        ArgumentNotValid.checkNotNull(url, "JMXServiceURL url");
        ArgumentNotValid.checkNotNull(credentials, "Map<String,String[]> credentials");
        try {
            ensureJndiInitialContext();
            return JMXConnectorFactory.connect(url, credentials).getMBeanServerConnection();
        } catch (IOException e) {
            throw new IOFailure("Could not connect to " + url.toString(), e);
        }
    }

    /**
     * Packages credentials as an environment for JMX connections.
     * This packaging has the same form that JConsole uses:
     * a one-entry Map, the mapping of "jmx.remote.credentials" being
     * an array containing the user name and the password.
     * @param userName The user to login as
     * @param password The password to use for that user
     * @return the packaged credentials
     */
    public static Map<String, String[]> packageCredentials(String userName, String password) {
        ArgumentNotValid.checkNotNullOrEmpty(userName, "String userName");
        ArgumentNotValid.checkNotNullOrEmpty(password, "String password");
        Map<String, String[]> credentials = new HashMap<String, String[]>(1);
        credentials.put("jmx.remote.credentials", new String[] { userName, password });
        return credentials;
    }

    /** Execute a command on a bean.
     *
     * @param connection Connection to the server holding the bean.
     * @param beanName Name of the bean.
     * @param command Command to execute.
     * @param arguments Arguments to the command.  Only string arguments are
     * possible at the moment.
     * @return The return value of the executed command.
     */
    public static Object executeCommand(MBeanServerConnection connection, String beanName, String command,
            String... arguments) {
        ArgumentNotValid.checkNotNull(connection, "MBeanServerConnection connection");
        ArgumentNotValid.checkNotNullOrEmpty(beanName, "String beanName");
        ArgumentNotValid.checkNotNullOrEmpty(command, "String command");
        ArgumentNotValid.checkNotNull(arguments, "String... arguments");

        log.debug(
                "Preparing to execute " + command + " with args " + Arrays.toString(arguments) + " on " + beanName);
        final int maxJmxRetries = getMaxTries();
        try {
            final String[] signature = new String[arguments.length];
            Arrays.fill(signature, String.class.getName());
            // The first time we attempt to connect to an mbean, we might have
            // to wait a bit for it to appear
            Throwable lastException;
            int tries = 0;
            do {
                tries++;
                try {
                    Object ret = connection.invoke(getBeanName(beanName), command, arguments, signature);
                    log.debug("Executed command " + command + " returned " + ret);
                    return ret;
                } catch (InstanceNotFoundException e) {
                    lastException = e;
                    if (tries < maxJmxRetries) {
                        TimeUtils.exponentialBackoffSleep(tries);
                    }
                } catch (IOException e) {
                    log.warn("Exception thrown while executing " + command + " with args "
                            + Arrays.toString(arguments) + " on " + beanName, e);
                    lastException = e;
                    if (tries < maxJmxRetries) {
                        TimeUtils.exponentialBackoffSleep(tries);
                    }
                }
            } while (tries < maxJmxRetries);
            throw new IOFailure("Failed to find MBean " + beanName + " for executing " + command + " after " + tries
                    + " attempts", lastException);
        } catch (MBeanException e) {
            throw new IOFailure("MBean exception for " + beanName, e);
        } catch (ReflectionException e) {
            throw new IOFailure("Reflection exception for " + beanName, e);
        }
    }

    /** Get the value of an attribute from a bean.
     *
     * @param beanName Name of the bean to get an attribute for.
     * @param attribute Name of the attribute to get.
     * @param connection A connection to the JMX server for the bean.
     * @return Value of the attribute.
     */
    public static Object getAttribute(String beanName, String attribute, MBeanServerConnection connection) {
        ArgumentNotValid.checkNotNullOrEmpty(beanName, "String beanName");
        ArgumentNotValid.checkNotNullOrEmpty(attribute, "String attribute");
        ArgumentNotValid.checkNotNull(connection, "MBeanServerConnection connection");

        log.debug("Preparing to get attribute " + attribute + " on " + beanName);
        final int maxJmxRetries = getMaxTries();
        try {
            // The first time we attempt to connect to an mbean, we might have
            // to wait a bit for it to appear
            Throwable lastException;
            int tries = 0;
            do {
                tries++;
                try {
                    Object ret = connection.getAttribute(getBeanName(beanName), attribute);
                    log.debug("Getting attribute " + attribute + " returned " + ret);
                    return ret;
                } catch (InstanceNotFoundException e) {
                    log.trace("Error while getting attribute " + attribute + " on " + beanName, e);
                    lastException = e;
                    if (tries < maxJmxRetries) {
                        TimeUtils.exponentialBackoffSleep(tries);
                    }
                } catch (IOException e) {
                    log.trace("Error while getting attribute " + attribute + " on " + beanName, e);
                    lastException = e;
                    if (tries < maxJmxRetries) {
                        TimeUtils.exponentialBackoffSleep(tries);
                    }
                }
            } while (tries < maxJmxRetries);
            throw new IOFailure("Failed to find MBean " + beanName + " for getting attribute " + attribute
                    + " after " + tries + " attempts", lastException);
        } catch (AttributeNotFoundException e) {
            throw new IOFailure("MBean exception for " + beanName, e);
        } catch (MBeanException e) {
            throw new IOFailure("MBean exception for " + beanName, e);
        } catch (ReflectionException e) {
            throw new IOFailure("Reflection exception for " + beanName, e);
        }
    }

    /** Get a bean name from a string version.
     *
     * @param beanName String representation of bean name
     * @return Object representing that bean name.
     */
    public static ObjectName getBeanName(String beanName) {
        ArgumentNotValid.checkNotNullOrEmpty(beanName, "String beanName");
        try {
            return new ObjectName(beanName);
        } catch (MalformedObjectNameException e) {
            throw new ArgumentNotValid("Name " + beanName + " is not a valid " + "object name", e);
        }
    }

    /** Get a JMXConnector to a given host and port, using login and password.
     *
     * @param hostName The host to attempt to connect to.
     * @param jmxPort The port on the host to connect to
     * (a non-negative number).
     * @param login The login name to authenticate as (typically "controlRole"
     * or "monitorRole".
     * @param password The password for JMX access.
     * @return A JMX connector to the given host and port, using default RMI.
     * @throws IOFailure if connecting to JMX fails.
     */
    public static JMXConnector getJMXConnector(String hostName, int jmxPort, final String login,
            final String password) {
        ArgumentNotValid.checkNotNullOrEmpty(hostName, "String hostName");
        ArgumentNotValid.checkNotNegative(jmxPort, "int jmxPort");
        ArgumentNotValid.checkNotNullOrEmpty(login, "String login");
        ArgumentNotValid.checkNotNullOrEmpty(password, "String password");

        JMXServiceURL rmiurl = getUrl(hostName, jmxPort, -1);
        Map<String, ?> environment = packageCredentials(login, password);
        Throwable lastException;
        int retries = 0;
        final int maxJmxRetries = getMaxTries();
        do {
            try {
                return JMXConnectorFactory.connect(rmiurl, environment);
            } catch (IOException e) {
                lastException = e;
                if (retries < maxJmxRetries && e.getCause() != null
                        && (e.getCause() instanceof ServiceUnavailableException
                                || e.getCause() instanceof SocketTimeoutException)) {
                    // Sleep a bit before trying again
                    TimeUtils.exponentialBackoffSleep(retries);
                    /*  called exponentialBackoffSleep(retries) which used
                    Calendar.MILLISECOND as time unit, which means we only
                    wait an exponential number of milliseconds.
                    */
                    continue;
                }
                break;
            }
        } while (retries++ < maxJmxRetries);
        throw new IOFailure(
                "Failed to connect to URL " + rmiurl + " after " + retries + " of " + maxJmxRetries
                        + " attempts.\nException type: " + lastException.getCause().getClass().getName(),
                lastException);
    }

    /** Get a single CompositeData object out of a TabularData structure.
     *
     * @param items TabularData structure as returned from JMX calls.
     * @return The one item in the items structure.
     * @throws ArgumentNotValid if there is not exactly one item in items,
     * or items is null.
     */
    public static CompositeData getOneCompositeData(TabularData items) {
        ArgumentNotValid.checkNotNull(items, "TabularData items");
        ArgumentNotValid.checkTrue(items.size() == 1, "TabularData items should have 1 item");
        return (CompositeData) items.values().toArray()[0];
    }

    /** Execute a single command, closing the connector afterwards.  If you
     * wish to hold on to the connector, call
     * JMXUtils#executeCommand(MBeanServerConnection, String, String, String[])
     *
     * @param connector A one-shot connector object.
     * @param beanName The name of the bean to execute a command on.
     * @param command The command to execute.
     * @param arguments The arguments to the command (all strings)
     * @return Whatever the command returned.
     */
    public static Object executeCommand(JMXConnector connector, String beanName, String command,
            String... arguments) {
        ArgumentNotValid.checkNotNull(connector, "JMXConnector connector");
        ArgumentNotValid.checkNotNullOrEmpty(beanName, "String beanName");
        ArgumentNotValid.checkNotNullOrEmpty(command, "String command");
        ArgumentNotValid.checkNotNull(arguments, "String... arguments");

        MBeanServerConnection connection;
        try {
            connection = connector.getMBeanServerConnection();
        } catch (IOException e) {
            throw new IOFailure("Failure getting JMX connection", e);
        }
        try {
            return executeCommand(connection, beanName, command, arguments);
        } finally {
            try {
                connector.close();
            } catch (IOException e) {
                log.warn("Couldn't close connection to " + beanName, e);
            }
        }
    }

    /** Get the value of an attribute, closing the connector afterwards.  If you
     * wish to hold on to the connector, call
     * JMXUtils#executeCommand(MBeanServerConnection, String, String, String[])
     *
     * @param connector A one-shot connector object.
     * @param beanName The name of the bean to get an attribute from.
     * @param attribute The attribute to get.
     * @return Whatever the command returned.
     */
    public static Object getAttribute(JMXConnector connector, String beanName, String attribute) {
        ArgumentNotValid.checkNotNull(connector, "JMXConnector connector");
        ArgumentNotValid.checkNotNullOrEmpty(beanName, "String beanName");
        ArgumentNotValid.checkNotNullOrEmpty(attribute, "String attribute");

        MBeanServerConnection connection;
        try {
            connection = connector.getMBeanServerConnection();
        } catch (IOException e) {
            throw new IOFailure("Failure getting JMX connection", e);
        }
        try {
            return getAttribute(beanName, attribute, connection);
        } finally {
            try {
                connector.close();
            } catch (IOException e) {
                log.warn("Couldn't close connection to " + beanName, e);
            }
        }
    }
}