All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.groupon.lex.metrics.jmx.JmxClient Maven / Gradle / Ivy

/*
 * Copyright (c) 2016, Groupon, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * Neither the name of GROUPON nor the names of its contributors may be
 * used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.groupon.lex.metrics.jmx;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import lombok.Getter;

/**
 *
 * @author ariane
 */
public class JmxClient implements AutoCloseable {
    public static interface ConnectionDecorator {
        public void apply(MBeanServerConnection conn) throws IOException;
    }

    private static final Logger LOG = Logger.getLogger(JmxClient.class.getName());
    @Getter
    private final Optional jmxUrl;
    private Optional jmx_factory_;
    private MBeanServerConnection conn_;  // conn_ == null -> connection needs recovery
    private final Collection recovery_callbacks_ = new ArrayList<>();

    /**
     * Create a default client, using the default PlatformMBeanServer.
     *
     * This client is effectively connected to the JVM it is part of.
     */
    public JmxClient() {
        jmxUrl = Optional.empty();
        jmx_factory_ = Optional.empty();
        conn_ = ManagementFactory.getPlatformMBeanServer();
    }

    /**
     * Create a new client, connecting to the specified URL.
     * @param connection_url the JMX server URL
     * @throws IOException if the connection cannot be established
     */
    public JmxClient(JMXServiceURL connection_url) throws IOException {
        jmxUrl = Optional.of(connection_url);
        jmx_factory_ = Optional.of(JMXConnectorFactory.newJMXConnector(jmxUrl.get(), null));
        try {
            jmx_factory_.get().connect();
        } catch (IOException ex) {
            LOG.log(Level.WARNING, "unable to connect");
            conn_ = null;
        }
        try {
            conn_ = jmx_factory_.get().getMBeanServerConnection();
        } catch (IOException ex) {
            LOG.log(Level.WARNING, "unable to connect");
            try {
                jmx_factory_.get().close();
            } catch (IOException ex1) {
                LOG.log(Level.WARNING, "unable to close failed connection attempt", ex1);
                /* Eat exception. */
            }
            conn_ = null;
        }
    }

    /**
     * Create a new client, connecting to the specified URL.
     * @param connection_url the JMX server URL, example: "service:jmx:rmi:///jndi/rmi://:9999/jmxrmi"
     * @throws MalformedURLException if the JMX URL is invalid
     * @throws IOException if the connection cannot be established
     */
    public JmxClient(String connection_url) throws MalformedURLException, IOException {
        this(new JMXServiceURL(connection_url));
    }

    /**
     * Returns a connection to the MBeanServer.
     *
     * Will restore connection if it has broken.
     * @return A connection to the MBean server.
     * @throws IOException if the connection couldn't be established.
     */
    public synchronized MBeanServerConnection getConnection() throws IOException {
        {
            Optional optionalConnection = getOptionalConnection();
            if (optionalConnection.isPresent()) return optionalConnection.get();
        }

        /* Re-create factory, because apparently they cannot re-create a broken connection. */
        if (jmxUrl.isPresent())
            jmx_factory_ = Optional.of(JMXConnectorFactory.newJMXConnector(jmxUrl.get(), null));  // May throw

        /* Attempt to recreate the connection and replay the listeners. */
        JMXConnector factory = jmx_factory_.orElseThrow(() -> new IOException("cannot recover from broken connection to local MBean Server"));
        factory.connect();  // May throw
        try {
            conn_ = factory.getMBeanServerConnection();
            for (ConnectionDecorator cb : recovery_callbacks_)
                cb.apply(conn_);
        } catch (IOException ex) {
            /* Cleanup on initialization failure. */
            conn_ = null;
            factory.close();
            throw ex;
        }
        return conn_;
    }

    /**
     * Get the connection, but don't bother with the recovery protocol if the connection is lost.
     * @return An optional with a connection, or empty optional indicating there is no connection.
     */
    public Optional getOptionalConnection() {
        if (conn_ != null) {
            /* Verify the connection is valid. */
            try {
                conn_.getMBeanCount();
            } catch (IOException ex) {
                try {
                    /* Connection is bad/lost. */
                    jmx_factory_
                            .orElseThrow(() -> new IOException("cannot recover from broken connection to local MBean Server"))
                            .close();
                } catch (IOException ex1) {
                    Logger.getLogger(JmxClient.class.getName()).log(Level.SEVERE, "failed to close failing connection", ex1);
                }
                conn_ = null;
            }
        }
        return Optional.ofNullable(conn_);
    }

    /**
     * Add a recovery callback.
     *
     * The callback will be invoked after adding it.
     * @param cb the callback that is to be added.
     */
    public void addRecoveryCallback(ConnectionDecorator cb) {
        if (cb == null) throw new NullPointerException("null callback");
        MBeanServerConnection conn;
        try {
            conn = getConnection();
        } catch (IOException ex) {
            Logger.getLogger(JmxClient.class.getName()).log(Level.FINE, "connection error while adding callback", ex);
            conn = null;
        }

        recovery_callbacks_.add(cb);
        try {
            if (conn != null) cb.apply(conn);
        } catch (IOException ex) {
            Logger.getLogger(getClass().getName()).log(Level.FINE, "connection error while adding callback", ex);
            /* Silently ignore: connection is bad and next time a connection is requested, the callback will be invoked. */

            /* Cleanup on initialization failure. */
            if (jmx_factory_.isPresent()) {
                conn_ = null;
                try {
                    jmx_factory_.get().close();
                } catch (IOException ex1) {
                    Logger.getLogger(JmxClient.class.getName()).log(Level.WARNING, "failed to close failing connection", ex1);
                }
            }
        }
    }

    /**
     * Remove a previously registered callback.
     * @param cb the callback that is to be removed.
     * @return the callback that was removed, or null is the callback was not found.
     */
    public ConnectionDecorator removeRecoveryCallback(ConnectionDecorator cb) {
        if (cb == null) throw new NullPointerException("null callback");
        return (recovery_callbacks_.remove(cb) ? cb : null);
    }

    @Override
    public synchronized void close() throws IOException {
        if (jmx_factory_.isPresent()) jmx_factory_.get().close();
        conn_ = null;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy