/*
* Copyright (C) 2015 SoftIndex LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.datakernel.jmx;
import com.google.inject.Key;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.management.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import static io.datakernel.util.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.util.Arrays.asList;
public final class JmxRegistry implements JmxRegistryMXBean {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static final String GENERIC_PARAM_NAME_FORMAT = "T%d=%s";
private final MBeanServer mbs;
private final DynamicMBeanFactory mbeanFactory;
// jmx
private int registeredSingletons;
private int registeredPools;
private int totallyRegisteredMBeans;
private double refreshPeriod;
private JmxRegistry(MBeanServer mbs, DynamicMBeanFactory mbeanFactory) {
this.mbs = checkNotNull(mbs);
this.mbeanFactory = checkNotNull(mbeanFactory);
this.refreshPeriod = 0.0;
}
public static JmxRegistry create(MBeanServer mbs, DynamicMBeanFactory mbeanFactory) {
return new JmxRegistry(mbs, mbeanFactory);
}
public JmxRegistry withRefreshPeriod(double refreshPeriod) {
this.refreshPeriod = refreshPeriod;
return this;
}
public void registerSingleton(Key<?> key, Object singletonInstance, MBeanSettings settings) {
checkNotNull(singletonInstance);
checkNotNull(key);
Class<?> instanceClass = singletonInstance.getClass();
Object mbean;
if (isJmxMBean(instanceClass)) {
try {
mbean = mbeanFactory.createFor(asList(singletonInstance), settings, true);
} catch (Exception e) {
String msg = format("Instance with key %s implemetns ConcurrentJmxMBean or EventloopJmxMBean" +
"but exception was thrown during attempt to create DynamicMBean", key.toString());
logger.error(msg, e);
return;
}
} else if (isStandardMBean(instanceClass) || isMXBean(instanceClass) || isDynamicMBean(instanceClass)) {
mbean = singletonInstance;
} else {
logger.info(format("Instance with key %s was not registered to jmx, " +
"because its type does not implement ConcurrentJmxMBean, EventloopJmxMBean " +
"and does not implement neither *MBean nor *MXBean interface", key.toString()));
return;
}
String name;
try {
name = createNameForKey(key);
} catch (ReflectiveOperationException e) {
String msg = format("Error during generation name for instance with key %s", key.toString());
logger.error(msg, e);
return;
}
ObjectName objectName;
try {
objectName = new ObjectName(name);
} catch (MalformedObjectNameException e) {
String msg = format("Cannot create ObjectName for instance with key %s. " +
"Proposed String name was \"%s\".", key.toString(), name);
logger.error(msg, e);
return;
}
try {
mbs.registerMBean(mbean, objectName);
logger.info(format("Instance with key %s was successfully registered to jmx " +
"with ObjectName \"%s\" ", key.toString(), objectName.toString()));
registeredSingletons++;
totallyRegisteredMBeans++;
} catch (NotCompliantMBeanException | InstanceAlreadyExistsException | MBeanRegistrationException e) {
String msg = format("Cannot register MBean for instance with key %s and ObjectName \"%s\"",
key.toString(), objectName.toString());
logger.error(msg, e);
return;
}
}
public void unregisterSingleton(Key<?> key, Object singletonInstance) {
checkNotNull(key);
if (isMBean(singletonInstance.getClass())) {
try {
String name = createNameForKey(key);
ObjectName objectName = new ObjectName(name);
mbs.unregisterMBean(objectName);
} catch (ReflectiveOperationException | JMException e) {
String msg =
format("Error during attempt to unregister MBean for instance with key %s.", key.toString());
logger.error(msg, e);
}
}
}
public void registerWorkers(Key<?> key, List<?> poolInstances, MBeanSettings settings) {
checkNotNull(poolInstances);
checkNotNull(key);
if (poolInstances.size() == 0) {
logger.info(format("Pool of instances with key %s is empty", key.toString()));
return;
}
if (!allInstancesAreOfSameType(poolInstances)) {
logger.info(format("Pool of instances with key %s was not registered to jmx " +
"because their types differ", key.toString()));
return;
}
if (!isJmxMBean(poolInstances.get(0).getClass())) {
logger.info(format("Pool of instances with key %s was not registered to jmx, " +
"because instances' type implements neither ConcurrentJmxMBean " +
"nor EventloopJmxMBean interface", key.toString()));
return;
}
String commonName;
try {
commonName = createNameForKey(key);
} catch (Exception e) {
String msg = format("Error during generation name for pool of instances with key %s", key.toString());
logger.error(msg, e);
return;
}
// register mbeans for each worker separately
for (int i = 0; i < poolInstances.size(); i++) {
MBeanSettings settingsForOptionals = MBeanSettings.of(
settings.getIncludedOptionals(), new HashMap<String, AttributeModifier<?>>());
registerMBeanForWorker(poolInstances.get(i), i, commonName, key, settingsForOptionals);
}
// register aggregated mbean for pool of workers
DynamicMBean mbean;
try {
mbean = mbeanFactory.createFor(poolInstances, settings, true);
} catch (Exception e) {
String msg = format("Cannot create DynamicMBean for aggregated MBean of pool of workers with key %s",
key.toString());
logger.error(msg, e);
return;
}
ObjectName objectName;
try {
objectName = new ObjectName(commonName);
} catch (MalformedObjectNameException e) {
String msg = format("Cannot create ObjectName for aggregated MBean of pool of workers with key %s. " +
"Proposed String name was \"%s\".", key.toString(), commonName);
logger.error(msg, e);
return;
}
try {
mbs.registerMBean(mbean, objectName);
logger.info(format("Pool of instances with key %s was successfully registered to jmx " +
"with ObjectName \"%s\"", key.toString(), objectName.toString()));
registeredPools++;
totallyRegisteredMBeans++;
} catch (NotCompliantMBeanException | InstanceAlreadyExistsException | MBeanRegistrationException e) {
String msg = format("Cannot register aggregated MBean of pool of workers with key %s " +
"and ObjectName \"%s\"", key.toString(), objectName.toString());
logger.error(msg, e);
return;
}
}
public void unregisterWorkers(Key<?> key, List<?> poolInstances) {
checkNotNull(key);
if (poolInstances.size() == 0) {
return;
}
if (!allInstancesAreOfSameType(poolInstances)) {
return;
}
if (!isJmxMBean(poolInstances.get(0).getClass())) {
return;
}
String commonName;
try {
commonName = createNameForKey(key);
} catch (ReflectiveOperationException e) {
String msg = format("Error during generation name for pool of instances with key %s", key.toString());
logger.error(msg, e);
return;
}
// unregister mbeans for each worker separately
for (int i = 0; i < poolInstances.size(); i++) {
try {
String workerName = createWorkerName(commonName, i);
mbs.unregisterMBean(new ObjectName(workerName));
} catch (JMException e) {
String msg = format("Error during attempt to unregister mbean for worker" +
" of pool of instances with key %s. Worker id is \"%d\"",
key.toString(), i);
logger.error(msg, e);
}
}
// unregister aggregated mbean for pool of workers
try {
mbs.unregisterMBean(new ObjectName(commonName));
} catch (JMException e) {
String msg = format("Error during attempt to unregister aggregated mbean for pool of instances " +
"with key %s.", key.toString());
logger.error(msg, e);
}
}
private boolean allInstancesAreOfSameType(List<?> instances) {
int last = instances.size() - 1;
for (int i = 0; i < last; i++) {
if (!instances.get(i).getClass().equals(instances.get(i + 1).getClass())) {
return false;
}
}
return true;
}
private void registerMBeanForWorker(Object worker, int workerId, String commonName,
Key<?> key, MBeanSettings settings) {
String workerName = createWorkerName(commonName, workerId);
DynamicMBean mbean;
try {
mbean = mbeanFactory.createFor(asList(worker), settings, false);
} catch (Exception e) {
String msg = format("Cannot create DynamicMBean for worker " +
"of pool of instances with key %s", key.toString());
logger.error(msg, e);
return;
}
ObjectName objectName;
try {
objectName = new ObjectName(workerName);
;
} catch (MalformedObjectNameException e) {
String msg = format("Cannot create ObjectName for worker of pool of instances with key %s. " +
"Proposed String name was \"%s\".", key.toString(), workerName);
logger.error(msg, e);
return;
}
try {
mbs.registerMBean(mbean, objectName);
totallyRegisteredMBeans++;
} catch (NotCompliantMBeanException | InstanceAlreadyExistsException | MBeanRegistrationException e) {
String msg = format("Cannot register MBean for worker of pool of instances with key %s. " +
"ObjectName for worker is \"%s\"", key.toString(), objectName.toString());
logger.error(msg, e);
return;
}
}
private static String createWorkerName(String commonName, int workerId) {
return commonName + format(",workerId=worker-%d", workerId);
}
private static String createNameForKey(Key<?> key) throws ReflectiveOperationException {
Class<?> rawType = key.getTypeLiteral().getRawType();
Annotation annotation = key.getAnnotation();
String domain = rawType.getPackage().getName();
String name = domain + ":";
if (annotation == null) { // without annotation
name += "type=" + rawType.getSimpleName();
} else {
Class<? extends Annotation> annotationType = annotation.annotationType();
Method[] annotationElements = filterNonEmptyElements(annotation);
if (annotationElements.length == 0) { // annotation without elements
name += "type=" + rawType.getSimpleName() + ",annotation=" + annotationType.getSimpleName();
} else if (annotationElements.length == 1 && annotationElements[0].getName().equals("value")) {
// annotation with single element which has name "value"
Object value = fetchAnnotationElementValue(annotation, annotationElements[0]);
name += annotationType.getSimpleName() + "=" + value.toString();
} else { // annotation with one or more custom elements
for (Method annotationParameter : annotationElements) {
Object value = fetchAnnotationElementValue(annotation, annotationParameter);
String nameKey = annotationParameter.getName();
String nameValue = value.toString();
name += nameKey + "=" + nameValue + ",";
}
assert name.substring(name.length() - 1).equals(",");
name = name.substring(0, name.length() - 1);
}
}
return addGenericParamsInfo(name, key);
}
private static String addGenericParamsInfo(String srcName, Key<?> key) {
Type type = key.getTypeLiteral().getType();
String resultName = srcName;
if (type instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) type;
Type[] genericArgs = pType.getActualTypeArguments();
for (int i = 0; i < genericArgs.length; i++) {
Type genericArg = genericArgs[i];
String argClassName = ((Class<?>) genericArg).getSimpleName();
int argId = i + 1;
resultName += "," + format(GENERIC_PARAM_NAME_FORMAT, argId, argClassName);
}
}
return resultName;
}
/**
* Returns values if it is not null, otherwise throws exception
*/
private static Object fetchAnnotationElementValue(Annotation annotation, Method element) throws ReflectiveOperationException {
Object value = element.invoke(annotation);
if (value == null) {
String errorMsg = "@" + annotation.annotationType().getName() + "." +
element.getName() + "() returned null";
throw new NullPointerException(errorMsg);
}
return value;
}
private static Method[] filterNonEmptyElements(Annotation annotation) throws ReflectiveOperationException {
List<Method> filtered = new ArrayList<>();
for (Method method : annotation.annotationType().getDeclaredMethods()) {
Object elementValue = fetchAnnotationElementValue(annotation, method);
if (elementValue instanceof String) {
String stringValue = (String) elementValue;
if (stringValue.length() == 0) {
// skip this element, because it is empty string
continue;
}
}
filtered.add(method);
}
return filtered.toArray(new Method[filtered.size()]);
}
private static boolean isStandardMBean(Class<?> clazz) {
return classImplementsInterfaceWithNameEndingWith(clazz, "MBean");
}
private static boolean isMXBean(Class<?> clazz) {
return classFollowsMXBeanConvention(clazz);
}
private static boolean classImplementsInterfaceWithNameEndingWith(Class<?> clazz, String ending) {
String clazzName = clazz.getSimpleName();
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> anInterface : interfaces) {
String interfaceName = anInterface.getSimpleName();
if (interfaceName.equals(clazzName + ending)) {
return true;
}
}
return false;
}
private static boolean classFollowsMXBeanConvention(Class<?> clazz) {
Class<?>[] interfazes = clazz.getInterfaces();
for (Class<?> interfaze : interfazes) {
if (interfaceFollowsMXBeanConvention(interfaze)) {
return true;
}
}
Class<?> superClazz = clazz.getSuperclass();
if (superClazz != null) {
return classFollowsMXBeanConvention(superClazz);
}
return false;
}
private static boolean interfaceFollowsMXBeanConvention(Class<?> interfaze) {
if (interfaze.getSimpleName().endsWith("MXBean") || interfaze.isAnnotationPresent(MXBean.class)) {
return true;
}
Class<?>[] subInterfazes = interfaze.getInterfaces();
for (Class<?> subInterfaze : subInterfazes) {
if (interfaceFollowsMXBeanConvention(subInterfaze)) {
return true;
}
}
return false;
}
private static boolean isJmxMBean(Class<?> clazz) {
return ConcurrentJmxMBean.class.isAssignableFrom(clazz) || EventloopJmxMBean.class.isAssignableFrom(clazz);
}
private static boolean isDynamicMBean(Class<?> clazz) {
return DynamicMBean.class.isAssignableFrom(clazz);
}
private static boolean isMBean(Class<?> clazz) {
return isJmxMBean(clazz) || isStandardMBean(clazz) || isMXBean(clazz) || isDynamicMBean(clazz);
}
// region jmx
@Override
public int getRegisteredSingletons() {
return registeredSingletons;
}
@Override
public int getRegisteredPools() {
return registeredPools;
}
@Override
public int getTotallyRegisteredMBeans() {
return totallyRegisteredMBeans;
}
@Override
public double getRefreshPeriod() {
return ((JmxMBeans) mbeanFactory).getSpecifiedRefreshPeriod();
}
@Override
public void setRefreshPeriod(double refreshPeriod) {
((JmxMBeans) mbeanFactory).setRefreshPeriod(refreshPeriod);
}
@Override
public int getMaxRefreshesPerOneCycle() {
return ((JmxMBeans) mbeanFactory).getMaxJmxRefreshesPerOneCycle();
}
@Override
public void setMaxRefreshesPerOneCycle(int maxRefreshesPerOneCycle) {
((JmxMBeans) mbeanFactory).setMaxJmxRefreshesPerOneCycle(maxRefreshesPerOneCycle);
}
@Override
public double[] getEffectiveRefreshPeriods() {
List<Integer> effectivePeriods =
new ArrayList<>(((JmxMBeans) mbeanFactory).getEffectiveRefreshPeriods().values());
double[] effectivePeriodsSeconds = new double[effectivePeriods.size()];
for (int i = 0; i < effectivePeriods.size(); i++) {
int periodMillis = effectivePeriods.get(i);
effectivePeriodsSeconds[i] = periodMillis / (double) 1000;
}
return effectivePeriodsSeconds;
}
@Override
public int[] getRefreshableStatsCount() {
List<Integer> counts = new ArrayList<>(((JmxMBeans) mbeanFactory).getRefreshableStatsCounts().values());
int[] refreshableStatsCountsArr = new int[counts.size()];
for (int i = 0; i < counts.size(); i++) {
Integer count = counts.get(i);
refreshableStatsCountsArr[i] = count;
}
return refreshableStatsCountsArr;
}
// endregion
}