/** * 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.jmx; import java.lang.management.ManagementFactory; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.management.MBeanServer; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import com.foundationdb.server.service.Service; public class JmxRegistryServiceImpl implements JmxRegistryService, JmxManageable, Service { private static final String FORMATTER = "com.foundationdb:type=%s"; private boolean started = false; private final Map<JmxManageable,ObjectName> serviceToName = new HashMap<>(); private final Map<ObjectName,JmxManageable> nameToService = new HashMap<>(); private final Object INTERNAL_LOCK = new Object(); private void addService(ObjectName objectName, JmxManageable service) { assert Thread.holdsLock(INTERNAL_LOCK) : "this method must be called from a synchronized block"; ObjectName oldName = serviceToName.put(service, objectName); JmxManageable oldService = nameToService.put(objectName, service); assert oldName == null : String.format("(%s) %s bumped %s", objectName, service, oldName); assert oldService == null : String.format("(%s) %s bumped %s", service, objectName, oldService); } private void removeService(ObjectName objectName) { assert Thread.holdsLock(INTERNAL_LOCK) : "this method must be called from a synchronized block"; JmxManageable removedService = nameToService.remove(objectName); assert removedService != null : "removed a null JmxManageble"; ObjectName removedName = serviceToName.remove(removedService); assert removedName.equals(objectName) : String.format("%s != %s", removedName, objectName); } @Override public ObjectName register(JmxManageable service) { final JmxObjectInfo info = service.getJmxObjectInfo(); validate(info); String serviceName = info.getObjectName(); if (!serviceName.matches("[\\w-]+")) { throw new JmxRegistrationException(service.getClass(), serviceName); } final ObjectName objectName; synchronized (INTERNAL_LOCK) { if (serviceToName.containsKey(service)) { throw new JmxRegistrationException("Already registered instance of " + service.getClass()); } objectName = getObjectName(serviceName, nameToService.keySet()); addService(objectName, service); if (started) { try { getMBeanServer().registerMBean(info.getInstance(), objectName); return objectName; } catch (Exception e) { removeService(objectName); throw new JmxRegistrationException(e); } } else { return objectName; } } } protected MBeanServer getMBeanServer() { return ManagementFactory.getPlatformMBeanServer(); } /** * Returns a unique object name. This method is <em>NOT</em> thread-safe; it's up to the caller to * provide any locking for consistency, atomicity, etc. Note that this method does not add the ObjectName * it generates to the set. * @param serviceName the service name * @param uniquenessSet the set that defines existing ObjectNames * @return an ObjectName that is not in the given set. */ private ObjectName getObjectName(String serviceName, Set<ObjectName> uniquenessSet) { assert Thread.holdsLock(INTERNAL_LOCK) : "this method must be called from a synchronized block"; ObjectName objectName; try { objectName = new ObjectName(String.format(FORMATTER, serviceName)); if (uniquenessSet.contains(objectName)) { throw new JmxRegistrationException("Bean name conflict: " + serviceName); } } catch (MalformedObjectNameException e) { throw new JmxRegistrationException(e); } return objectName; } @Override public void unregister(String objectNameString) { synchronized (INTERNAL_LOCK) { try { final ObjectName registeredObject = new ObjectName(String.format(FORMATTER, objectNameString)); if (started) { getMBeanServer().unregisterMBean(registeredObject); } removeService(registeredObject); } catch (Exception e) { throw new JmxRegistrationException(e); } } } @Override public void unregister(ObjectName registeredObject) { synchronized (INTERNAL_LOCK) { try { if (started) { getMBeanServer().unregisterMBean(registeredObject); } removeService(registeredObject); } catch (Exception e) { throw new JmxRegistrationException(e); } } } @Override public void start() { final MBeanServer mbs = getMBeanServer(); synchronized (INTERNAL_LOCK) { if (started) { return; } for (Map.Entry<JmxManageable,ObjectName> entry : serviceToName.entrySet()) { final JmxManageable service = entry.getKey(); final ObjectName objectName = entry.getValue(); try { mbs.registerMBean(service.getJmxObjectInfo().getInstance(), objectName); } catch (Exception e) { throw new JmxRegistrationException("for " + objectName, e); } } started = true; } } @Override public void stop() { final MBeanServer mbs = getMBeanServer(); synchronized (INTERNAL_LOCK) { if (!started) { return; } for (ObjectName objectName : nameToService.keySet()) { try { mbs.unregisterMBean(objectName); } catch (Exception e) { throw new JmxRegistrationException(e); } } started = false; } } @Override public void crash() { // Nice to unregister these so that a restarted instance can // register new instances. stop(); } @Override public JmxObjectInfo getJmxObjectInfo() { return new JmxObjectInfo("JmxManager", this, JmxRegistryServiceMXBean.class); } private static boolean isManagable(Class<?> theInterface) { return theInterface.getSimpleName().endsWith("MXBean"); } void validate(JmxObjectInfo objectInfo) { Class<?> jmxInterface = objectInfo.getManagedInterface(); if (!isManagable(jmxInterface)) { throw new JmxRegistrationException("Managed interface must end in \"MXBean\""); } assert jmxInterface.isAssignableFrom(objectInfo.getInstance().getClass()) : String.format("%s is not assignable from %s", jmxInterface, objectInfo.getInstance().getClass()); Set<Class<?>> objectInterfaces = getAllInterfaces(objectInfo.getInstance().getClass(), new HashSet<Class<?>>()); Iterator<Class<?>> interfacesIter = objectInterfaces.iterator(); while (interfacesIter.hasNext()) { if (!isManagable(interfacesIter.next())) { interfacesIter.remove(); } } if (objectInterfaces.size() != 1) { throw new JmxRegistrationException("Need exactly one *MXBean interface for " + objectInfo.getInstance().getClass() + ". Found: " + objectInterfaces); } } private static Set<Class<?>> getAllInterfaces(Class<?> root, Set<Class<?>> set) { if (root.isInterface()) { set.add(root); } getAllInterfaces(root.getInterfaces(), set); Class<?> rootSuper = root.getSuperclass(); if (rootSuper != null) { getAllInterfaces(rootSuper, set); } return set; } private static void getAllInterfaces(Class<?>[] roots, Set<Class<?>> set) { for (Class<?> root : roots) { assert root.isInterface() : root; if (!set.contains(root)) { set.add(root); getAllInterfaces(root.getInterfaces(), set); } } } }