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

io.datakernel.jmx.DynamicMBeanFactoryImpl Maven / Gradle / Ivy

/*
 * 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 io.datakernel.common.ref.Ref;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.eventloop.jmx.EventloopJmxMBean;
import io.datakernel.eventloop.jmx.JmxRefreshable;
import io.datakernel.eventloop.jmx.JmxRefreshableStats;
import io.datakernel.eventloop.jmx.JmxStats;
import io.datakernel.eventloop.util.ReflectionUtils;
import io.datakernel.jmx.api.*;
import io.datakernel.jmx.api.JmxReducers.JmxReducerDistinct;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.management.*;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.function.Function;

import static io.datakernel.common.Preconditions.checkArgument;
import static io.datakernel.common.Preconditions.checkNotNull;
import static io.datakernel.common.Utils.nullToDefault;
import static io.datakernel.common.collection.CollectionUtils.first;
import static io.datakernel.eventloop.util.ReflectionUtils.*;
import static io.datakernel.jmx.Utils.allInstancesAreOfSameType;
import static java.lang.Math.ceil;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;

@SuppressWarnings("rawtypes")
public final class DynamicMBeanFactoryImpl implements DynamicMBeanFactory {
	private static final Logger logger = LoggerFactory.getLogger(DynamicMBeanFactoryImpl.class);

	// refreshing jmx
	public static final Duration DEFAULT_REFRESH_PERIOD_IN_SECONDS = Duration.ofSeconds(1);
	public static final int MAX_JMX_REFRESHES_PER_ONE_CYCLE_DEFAULT = 500;
	private int maxJmxRefreshesPerOneCycle;
	private Duration specifiedRefreshPeriod;
	private final Map> eventloopToJmxRefreshables = new ConcurrentHashMap<>();
	private final Map refreshableStatsCounts = new ConcurrentHashMap<>();
	private final Map effectiveRefreshPeriods = new ConcurrentHashMap<>();

	private static final JmxReducer DEFAULT_REDUCER = new JmxReducerDistinct();

	// JmxStats creator methods
	private static final String CREATE = "create";
	private static final String CREATE_ACCUMULATOR = "createAccumulator";

	private static final DynamicMBeanFactoryImpl INSTANCE_WITH_DEFAULT_REFRESH_PERIOD = new DynamicMBeanFactoryImpl(DEFAULT_REFRESH_PERIOD_IN_SECONDS, MAX_JMX_REFRESHES_PER_ONE_CYCLE_DEFAULT);

	// region constructor and factory methods
	private DynamicMBeanFactoryImpl(@NotNull Duration refreshPeriod, int maxJmxRefreshesPerOneCycle) {
		this.specifiedRefreshPeriod = refreshPeriod;
		this.maxJmxRefreshesPerOneCycle = maxJmxRefreshesPerOneCycle;
	}

	public static DynamicMBeanFactoryImpl create() {
		return INSTANCE_WITH_DEFAULT_REFRESH_PERIOD;
	}

	public static DynamicMBeanFactoryImpl create(Duration refreshPeriod, int maxJmxRefreshesPerOneCycle) {
		return new DynamicMBeanFactoryImpl(refreshPeriod, maxJmxRefreshesPerOneCycle);
	}
	// endregion

	// region exportable stats for JmxRegistry
	public Map getRefreshableStatsCounts() {
		return refreshableStatsCounts;
	}

	public Map getEffectiveRefreshPeriods() {
		return effectiveRefreshPeriods;
	}

	public Duration getSpecifiedRefreshPeriod() {
		return specifiedRefreshPeriod;
	}

	public void setRefreshPeriod(Duration refreshPeriod) {
		this.specifiedRefreshPeriod = refreshPeriod;
	}

	public int getMaxJmxRefreshesPerOneCycle() {
		return maxJmxRefreshesPerOneCycle;
	}

	public void setMaxJmxRefreshesPerOneCycle(int maxJmxRefreshesPerOneCycle) {
		this.maxJmxRefreshesPerOneCycle = maxJmxRefreshesPerOneCycle;
	}
	// endregion

	/**
	 * Creates Jmx MBean for monitorables with operations and attributes.
	 */
	@Override
	public DynamicMBean createDynamicMBean(@NotNull List monitorables, @NotNull MBeanSettings setting, boolean enableRefresh) {
		checkArgument(monitorables.size() > 0, "Size of list of monitorables should be greater than 0");
		checkArgument(monitorables.stream().noneMatch(Objects::isNull), "Monitorable can not be null");
		checkArgument(allInstancesAreOfSameType(monitorables), "Monitorables should be of the same type");

		Object firstMBean = monitorables.get(0);
		Class mbeanClass = firstMBean.getClass();

		boolean isRefreshEnabled = enableRefresh;

		List mbeanWrappers = new ArrayList<>(monitorables.size());
		if (ConcurrentJmxMBean.class.isAssignableFrom(mbeanClass)) {
			checkArgument(monitorables.size() == 1, "ConcurrentJmxMBeans cannot be used in pool. " +
					"Only EventloopJmxMBeans can be used in pool");
			isRefreshEnabled = false;
			mbeanWrappers.add(new ConcurrentJmxMBeanWrapper((ConcurrentJmxMBean) monitorables.get(0)));
		} else if (EventloopJmxMBean.class.isAssignableFrom(mbeanClass)) {
			for (Object monitorable : monitorables) {
				mbeanWrappers.add(new EventloopJmxMBeanWrapper((EventloopJmxMBean) monitorable));
			}
		} else {
			throw new IllegalArgumentException("MBeans should implement either ConcurrentJmxMBean " +
					"or EventloopJmxMBean interface");
		}

		AttributeNodeForPojo rootNode = createAttributesTree(mbeanClass, setting.getCustomTypes());
		rootNode.hideNullPojos(monitorables);

		for (String included : setting.getIncludedOptionals()) {
			rootNode.setVisible(included);
		}

		// TODO(vmykhalko): check in JmxRegistry that modifiers are applied only once in case of workers and pool registartion
		for (String attrName : setting.getModifiers().keySet()) {
			AttributeModifier modifier = setting.getModifiers().get(attrName);
			try {
				rootNode.applyModifier(attrName, modifier, monitorables);
			} catch (ClassCastException e) {
				//noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException - doesn't ignore
				throw new IllegalArgumentException("Cannot apply modifier \"" + modifier.getClass().getName() +
						"\" for attribute \"" + attrName + "\": " + e.toString());
			}
		}

		MBeanInfo mBeanInfo = createMBeanInfo(rootNode, mbeanClass);
		Map opkeyToMethod = fetchOpkeyToMethod(mbeanClass);

		DynamicMBeanAggregator mbean = new DynamicMBeanAggregator(mBeanInfo, mbeanWrappers, rootNode, opkeyToMethod);

		// TODO(vmykhalko): maybe try to get all attributes and log warn message in case of exception? (to prevent potential errors during viewing jmx stats using jconsole)
//		tryGetAllAttributes(mbean);

		if (isRefreshEnabled) {
			handleJmxRefreshables(mbeanWrappers, rootNode);
		}
		return mbean;
	}

	// region building tree of AttributeNodes
	private List createNodesFor(Class clazz, Class mbeanClass,
			String[] includedOptionalAttrs, @Nullable Method getter,
			Map> customTypes) {

		Set includedOptionals = new HashSet<>(asList(includedOptionalAttrs));
		List attrDescriptors = fetchAttributeDescriptors(clazz, customTypes);
		List attrNodes = new ArrayList<>();
		for (AttributeDescriptor descriptor : attrDescriptors) {
			checkNotNull(descriptor.getGetter(), "@JmxAttribute \"%s\" does not have getter", descriptor.getName());

			String attrName;
			Method attrGetter = descriptor.getGetter();
			JmxAttribute attrAnnotation = attrGetter.getAnnotation(JmxAttribute.class);
			String attrAnnotationName = attrAnnotation.name();
			if (attrAnnotationName.equals(JmxAttribute.USE_GETTER_NAME)) {
				attrName = extractFieldNameFromGetter(attrGetter);
			} else {
				attrName = attrAnnotationName;
			}
			checkArgument(!attrName.contains("_"), "@JmxAttribute with name \"%s\" contains underscores", attrName);

			String attrDescription = null;
			if (!attrAnnotation.description().equals(JmxAttribute.NO_DESCRIPTION)) {
				attrDescription = attrAnnotation.description();
			}

			boolean included = !attrAnnotation.optional() || includedOptionals.contains(attrName);
			includedOptionals.remove(attrName);

			Type type = attrGetter.getGenericReturnType();
			Method attrSetter = descriptor.getSetter();
			AttributeNode attrNode = createAttributeNodeFor(attrName, attrDescription, type, included,
					attrAnnotation, attrGetter, attrSetter, mbeanClass,
					customTypes);
			attrNodes.add(attrNode);
		}

		if (includedOptionals.size() > 0) {
			assert getter != null; // in this case getter cannot be null
			throw new RuntimeException(format("Error in \"extraSubAttributes\" parameter in @JmxAnnotation" +
							" on %s.%s(). There is no field \"%s\" in %s.",
					getter.getDeclaringClass().getName(), getter.getName(),
					first(includedOptionals), getter.getReturnType().getName()));
		}

		return attrNodes;
	}

	private List fetchAttributeDescriptors(Class clazz, Map> customTypes) {
		Map nameToAttr = new HashMap<>();
		for (Method method : clazz.getMethods()) {
			if (method.isAnnotationPresent(JmxAttribute.class)) {
				if (isGetter(method)) {
					processGetter(nameToAttr, method);
				} else if (isSetter(method)) {
					processSetter(nameToAttr, method, customTypes);
				} else {
					throw new RuntimeException(format("Method \"%s\" of class \"%s\" is annotated with @JmxAnnotation "
							+ "but is neither getter nor setter", method.getName(), method.getClass().getName())
					);
				}
			}
		}
		return new ArrayList<>(nameToAttr.values());
	}

	private static void processGetter(Map nameToAttr, Method getter) {
		String name = extractFieldNameFromGetter(getter);
		Type attrType = getter.getReturnType();
		if (nameToAttr.containsKey(name)) {
			AttributeDescriptor previousDescriptor = nameToAttr.get(name);

			checkArgument(previousDescriptor.getGetter() == null,
					"More that one getter with name" + getter.getName());
			checkArgument(previousDescriptor.getType().equals(attrType),
					"Getter with name \"%s\" has different type than appropriate setter", getter.getName());

			nameToAttr.put(name, new AttributeDescriptor(name, attrType, getter, previousDescriptor.getSetter()));
		} else {
			nameToAttr.put(name, new AttributeDescriptor(name, attrType, getter, null));
		}
	}

	private void processSetter(Map nameToAttr, Method setter, Map> customTypes) {
		Class attrType = setter.getParameterTypes()[0];
		checkArgument(ReflectionUtils.isSimpleType(attrType) || customTypes.containsKey(attrType), "Setters are allowed only on SimpleType attributes."
				+ " But setter \"%s\" is not SimpleType setter", setter.getName());

		String name = extractFieldNameFromSetter(setter);

		if (nameToAttr.containsKey(name)) {
			AttributeDescriptor previousDescriptor = nameToAttr.get(name);

			checkArgument(previousDescriptor.getSetter() == null,
					"More that one setter with name" + setter.getName());
			checkArgument(previousDescriptor.getType().equals(attrType),
					"Setter with name \"%s\" has different type than appropriate getter", setter.getName());

			nameToAttr.put(name, new AttributeDescriptor(
					name, attrType, previousDescriptor.getGetter(), setter));
		} else {
			nameToAttr.put(name, new AttributeDescriptor(name, attrType, null, setter));
		}
	}

	@SuppressWarnings("unchecked")
	private AttributeNode createAttributeNodeFor(
			String attrName,
			@Nullable String attrDescription,
			Type attrType,
			boolean included,
			@Nullable JmxAttribute attrAnnotation,
			@Nullable Method getter,
			@Nullable Method setter,
			Class mbeanClass,
			Map> customTypes) {
		if (attrType instanceof Class) {
			ValueFetcher defaultFetcher = createAppropriateFetcher(getter);
			// 4 cases: custom-type, simple-type, JmxRefreshableStats, POJO
			Class returnClass = (Class) attrType;

			if (customTypes.containsKey(attrType)) {
				JmxCustomTypeAdapter customTypeAdapter = customTypes.get(attrType);
				return new AttributeNodeForConverterType(attrName, attrDescription, included,
						defaultFetcher, setter, customTypeAdapter.to, customTypeAdapter.from);

			} else if (ReflectionUtils.isSimpleType(returnClass)) {
				JmxReducer reducer;
				try {
					reducer = fetchReducerFrom(getter);
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
				return new AttributeNodeForSimpleType(
						attrName, attrDescription, included, defaultFetcher, setter, returnClass, reducer
				);

			} else if (isThrowable(returnClass)) {
				return new AttributeNodeForThrowable(attrName, attrDescription, included, defaultFetcher);

			} else if (returnClass.isArray()) {
				Class elementType = returnClass.getComponentType();
				checkNotNull(getter, "Arrays can be used only directly in POJO, JmxRefreshableStats or JmxMBeans");
				ValueFetcher fetcher = new ValueFetcherFromGetterArrayAdapter(getter);
				return createListAttributeNodeFor(attrName, attrDescription, included, fetcher, elementType, mbeanClass, customTypes);

			} else if (isJmxStats(returnClass)) {
				// JmxRefreshableStats case

				checkJmxStatsAreValid(returnClass, mbeanClass, getter);

				String[] extraSubAttributes =
						attrAnnotation != null ? attrAnnotation.extraSubAttributes() : new String[0];
				List subNodes =
						createNodesFor(returnClass, mbeanClass, extraSubAttributes, getter, customTypes);

				if (subNodes.size() == 0) {
					throw new IllegalArgumentException(format(
							"JmxRefreshableStats of type \"%s\" does not have JmxAttributes",
							returnClass.getName()));
				}

				return new AttributeNodeForPojo(attrName, attrDescription, included, defaultFetcher,
						createReducerForJmxStats(returnClass), subNodes);

			} else {
				String[] extraSubAttributes =
						attrAnnotation != null ? attrAnnotation.extraSubAttributes() : new String[0];
				List subNodes =
						createNodesFor(returnClass, mbeanClass, extraSubAttributes, getter, customTypes);

				if (subNodes.size() == 0) {
					throw new IllegalArgumentException("Unrecognized type of Jmx attribute: " + attrType.getTypeName());
				} else {
					// POJO case

					JmxReducer reducer;
					try {
						reducer = fetchReducerFrom(getter);
					} catch (Exception e) {
						throw new RuntimeException(e);
					}

					return new AttributeNodeForPojo(
							attrName, attrDescription, included, defaultFetcher, reducer == DEFAULT_REDUCER ? null : reducer, subNodes);
				}
			}
		} else if (attrType instanceof ParameterizedType) {
			return createNodeForParametrizedType(
					attrName, attrDescription, (ParameterizedType) attrType, included, getter, setter, mbeanClass, customTypes);
		} else {
			throw new IllegalArgumentException("Unrecognized type of Jmx attribute: " + attrType.getTypeName());
		}
	}

	@SuppressWarnings("unchecked")
	private static JmxReducer createReducerForJmxStats(Class jmxStatsClass) {
		return (JmxReducer) sources -> {
			JmxStats accumulator = createJmxAccumulator(jmxStatsClass);
			for (Object pojo : sources) {
				JmxStats jmxStats = (JmxStats) pojo;
				if (jmxStats != null) {
					accumulator.add(jmxStats);
				}
			}
			return accumulator;
		};
	}

	private static JmxStats createJmxAccumulator(Class jmxStatsClass) {
		JmxStats jmxStats = ReflectionUtils.tryToCreateInstanceWithFactoryMethods(jmxStatsClass, CREATE_ACCUMULATOR, CREATE);
		if (jmxStats == null) {
			throw new RuntimeException("Cannot create JmxStats accumulator instance: " + jmxStatsClass.getName());
		}
		return jmxStats;
	}

	@SuppressWarnings("unchecked")
	private static JmxReducer fetchReducerFrom(@Nullable Method getter) throws IllegalAccessException, InstantiationException {
		if (getter == null) {
			return DEFAULT_REDUCER;
		}
		JmxAttribute attrAnnotation = getter.getAnnotation(JmxAttribute.class);
		Class reducerClass = attrAnnotation.reducer();
		if (reducerClass == DEFAULT_REDUCER.getClass()) {
			return DEFAULT_REDUCER;
		}
		return ((Class>) reducerClass).newInstance();
	}

	private static void checkJmxStatsAreValid(Class returnClass, Class mbeanClass, @Nullable Method getter) {
		if (JmxRefreshableStats.class.isAssignableFrom(returnClass) &&
				!EventloopJmxMBean.class.isAssignableFrom(mbeanClass)) {
			logger.warn("JmxRefreshableStats won't be refreshed when used in classes that do not implement" +
					" EventloopJmxMBean. MBean class: " + mbeanClass.getName());
		}

		if (returnClass.isInterface()) {
			throw new IllegalArgumentException(createErrorMessageForInvalidJmxStatsAttribute(getter));
		}

		if (Modifier.isAbstract(returnClass.getModifiers())) {
			throw new IllegalArgumentException(createErrorMessageForInvalidJmxStatsAttribute(getter));
		}

		if (!canBeCreated(returnClass, CREATE_ACCUMULATOR, CREATE)) {
			throw new IllegalArgumentException(createErrorMessageForInvalidJmxStatsAttribute(getter));
		}
	}

	private static String createErrorMessageForInvalidJmxStatsAttribute(@Nullable Method getter) {
		String msg = "Return type of JmxRefreshableStats attribute must be concrete class that implements" +
				" JmxRefreshableStats interface and contains" +
				" static factory \"" + CREATE_ACCUMULATOR + "()\" method or" +
				" static factory \"" + CREATE + "()\" method or" +
				" public no-arg constructor";

		if (getter != null) {
			msg += format(". Error at %s.%s()", getter.getDeclaringClass().getName(), getter.getName());
		}
		return msg;
	}

	private AttributeNode createNodeForParametrizedType(String attrName, @Nullable String attrDescription,
			ParameterizedType pType, boolean included,
			@Nullable Method getter, @Nullable Method setter, Class mbeanClass,
			Map> customTypes) {
		ValueFetcher fetcher = createAppropriateFetcher(getter);
		Class rawType = (Class) pType.getRawType();

		if (rawType == List.class) {
			Type listElementType = pType.getActualTypeArguments()[0];
			return createListAttributeNodeFor(
					attrName, attrDescription, included, fetcher, listElementType, mbeanClass, customTypes);
		} else if (rawType == Map.class) {
			Type valueType = pType.getActualTypeArguments()[1];
			return createMapAttributeNodeFor(attrName, attrDescription, included, fetcher, valueType, mbeanClass, customTypes);
		} else if (customTypes.containsKey(rawType)) {
			return createConverterAttributeNodeFor(attrName, attrDescription, pType, included, fetcher, setter, customTypes);
		} else {
			throw new IllegalArgumentException("There is no support for generic class " + pType.getTypeName());
		}
	}

	@SuppressWarnings("unchecked")
	private AttributeNodeForConverterType createConverterAttributeNodeFor(
			String attrName,
			@Nullable String attrDescription,
			ParameterizedType type, boolean included,
			ValueFetcher fetcher, @Nullable Method setter,
			Map> customTypes) {
		Type[] actualTypes = type.getActualTypeArguments();
		for (Type genericType : actualTypes) {
			if (!customTypes.containsKey(genericType)) {
				throw new IllegalArgumentException("There is no support for generic type " + type.getTypeName());
			}
		}
		JmxCustomTypeAdapter t = customTypes.get(type.getRawType());
		return new AttributeNodeForConverterType(attrName, attrDescription, fetcher, included, setter, t.to, t.from);
	}

	private AttributeNodeForList createListAttributeNodeFor(
			String attrName,
			@Nullable String attrDescription,
			boolean included,
			ValueFetcher fetcher,
			Type listElementType, Class mbeanClass,
			Map> customTypes) {
		if (listElementType instanceof Class) {
			Class listElementClass = (Class) listElementType;
			boolean isListOfJmxRefreshable = JmxRefreshable.class.isAssignableFrom(listElementClass);
			return new AttributeNodeForList(
					attrName,
					attrDescription,
					included,
					fetcher,
					createAttributeNodeFor("", attrDescription, listElementType, true, null, null, null, mbeanClass, customTypes),
					isListOfJmxRefreshable
			);
		} else if (listElementType instanceof ParameterizedType) {
			String typeName = ((Class) ((ParameterizedType) listElementType).getRawType()).getSimpleName();
			return new AttributeNodeForList(
					attrName,
					attrDescription,
					included,
					fetcher,
					createNodeForParametrizedType(
							typeName, attrDescription, (ParameterizedType) listElementType, true, null, null, mbeanClass,
							customTypes
					),
					false
			);
		} else {
			throw new IllegalArgumentException("Can't create list attribute node for List<" + listElementType.getTypeName() + ">");
		}
	}

	private AttributeNodeForMap createMapAttributeNodeFor(
			String attrName,
			@Nullable String attrDescription,
			boolean included, ValueFetcher fetcher,
			Type valueType, Class mbeanClass,
			Map> customTypes) {
		boolean isMapOfJmxRefreshable = false;
		AttributeNode node;
		if (valueType instanceof Class) {
			Class valueClass = (Class) valueType;
			isMapOfJmxRefreshable = JmxRefreshable.class.isAssignableFrom(valueClass);
			node = createAttributeNodeFor("", attrDescription, valueType, true, null, null, null, mbeanClass, customTypes);
		} else if (valueType instanceof ParameterizedType) {
			String typeName = ((Class) ((ParameterizedType) valueType).getRawType()).getSimpleName();
			node = createNodeForParametrizedType(typeName, attrDescription, (ParameterizedType) valueType, true, null, null, mbeanClass,
					customTypes);
		} else {
			throw new IllegalArgumentException("Can't create map attribute node for " + valueType.getTypeName());
		}
		return new AttributeNodeForMap(attrName, attrDescription, included, fetcher, node, isMapOfJmxRefreshable);
	}

	private static ValueFetcher createAppropriateFetcher(@Nullable Method getter) {
		return getter != null ? new ValueFetcherFromGetter(getter) : new ValueFetcherDirect();
	}
	// endregion

	// region refreshing jmx
	private void handleJmxRefreshables(List mbeanWrappers, AttributeNodeForPojo rootNode) {
		for (MBeanWrapper mbeanWrapper : mbeanWrappers) {
			Eventloop eventloop = mbeanWrapper.getEventloop();
			List currentRefreshables = rootNode.getAllRefreshables(mbeanWrapper.getMBean());
			if (!eventloopToJmxRefreshables.containsKey(eventloop)) {
				eventloopToJmxRefreshables.put(eventloop, currentRefreshables);
				eventloop.execute(createRefreshTask(eventloop, null, 0));
			} else {
				List previousRefreshables = eventloopToJmxRefreshables.get(eventloop);
				List allRefreshables = new ArrayList<>(previousRefreshables);
				allRefreshables.addAll(currentRefreshables);
				eventloopToJmxRefreshables.put(eventloop, allRefreshables);
			}

			refreshableStatsCounts.put(eventloop, eventloopToJmxRefreshables.get(eventloop).size());
		}
	}

	private Runnable createRefreshTask(Eventloop eventloop, @Nullable List previousList, int previousRefreshes) {
		return () -> {
			long currentTime = eventloop.currentTimeMillis();

			List jmxRefreshableList = previousList;
			if (jmxRefreshableList == null) {
				// list might be updated in case of several mbeans in one eventloop
				jmxRefreshableList = eventloopToJmxRefreshables.get(eventloop);
				effectiveRefreshPeriods.put(eventloop, (int) computeEffectiveRefreshPeriod(jmxRefreshableList.size()));
			}

			int currentRefreshes = 0;
			while (currentRefreshes < maxJmxRefreshesPerOneCycle) {
				int index = currentRefreshes + previousRefreshes;
				if (index == jmxRefreshableList.size()) {
					break;
				}
				jmxRefreshableList.get(index).refresh(currentTime);
				currentRefreshes++;
			}

			long nextTimestamp = currentTime + computeEffectiveRefreshPeriod(jmxRefreshableList.size());
			int totalRefreshes = currentRefreshes + previousRefreshes;
			if (totalRefreshes == jmxRefreshableList.size()) {
				eventloop.scheduleBackground(nextTimestamp, createRefreshTask(eventloop, null, 0));
			} else {
				eventloop.scheduleBackground(nextTimestamp, createRefreshTask(eventloop, jmxRefreshableList, totalRefreshes));
			}
		};
	}

	private long computeEffectiveRefreshPeriod(int jmxRefreshablesCount) {
		if (jmxRefreshablesCount == 0) {
			return specifiedRefreshPeriod.toMillis();
		}
		double ratio = ceil(jmxRefreshablesCount / (double) maxJmxRefreshesPerOneCycle);
		return (long) (specifiedRefreshPeriod.toMillis() / ratio);
	}

	/**
	 * Creates attribute tree of Jmx attributes for clazz.
	 */
	private AttributeNodeForPojo createAttributesTree(Class clazz, Map> customTypes) {
		List subNodes = createNodesFor(clazz, clazz, new String[0], null, customTypes);
		return new AttributeNodeForPojo("", null, true, new ValueFetcherDirect(), null, subNodes);
	}
	// endregion

	// region creating jmx metadata - MBeanInfo
	private static MBeanInfo createMBeanInfo(AttributeNodeForPojo rootNode, Class monitorableClass) {
		String monitorableName = "";
		String monitorableDescription = "";
		MBeanAttributeInfo[] attributes = rootNode != null ?
				fetchAttributesInfo(rootNode) :
				new MBeanAttributeInfo[0];
		MBeanOperationInfo[] operations = fetchOperationsInfo(monitorableClass);
		return new MBeanInfo(
				monitorableName,
				monitorableDescription,
				attributes,
				null,  // constructors
				operations,
				null); //notifications
	}

	private static MBeanAttributeInfo[] fetchAttributesInfo(AttributeNodeForPojo rootNode) {
		Set visibleAttrs = rootNode.getVisibleAttributes();
		Map> nameToType = rootNode.getOpenTypes();
		Map> nameToDescriptions = rootNode.getDescriptions();
		List attrsInfo = new ArrayList<>();
		for (String attrName : visibleAttrs) {
			String description = createDescription(attrName, nameToDescriptions.get(attrName));
			OpenType attrType = nameToType.get(attrName);
			boolean writable = rootNode.isSettable(attrName);
			boolean isIs = attrType.equals(SimpleType.BOOLEAN);
			attrsInfo.add(new MBeanAttributeInfo(attrName, attrType.getClassName(), description, true, writable, isIs));
		}

		return attrsInfo.toArray(new MBeanAttributeInfo[0]);
	}

	private static String createDescription(String name, Map groupDescriptions) {
		if (groupDescriptions.isEmpty()) {
			return name;
		}

		if (!name.contains("_")) {
			assert groupDescriptions.size() == 1;
			return first(groupDescriptions.values());
		}

		String descriptionTemplate = "\"%s\": %s";
		String separator = "  |  ";
		StringBuilder totalDescription = new StringBuilder();
		for (String groupName : groupDescriptions.keySet()) {
			String groupDescription = groupDescriptions.get(groupName);
			totalDescription.append(String.format(descriptionTemplate, groupName, groupDescription));
			totalDescription.append(separator);
		}
		totalDescription.delete(totalDescription.length() - separator.length(), totalDescription.length());
		return totalDescription.toString();
	}

	private static MBeanOperationInfo[] fetchOperationsInfo(Class monitorableClass) {
		List operations = new ArrayList<>();
		Method[] methods = monitorableClass.getMethods();
		for (Method method : methods) {
			if (method.isAnnotationPresent(JmxOperation.class)) {
				JmxOperation annotation = method.getAnnotation(JmxOperation.class);
				String opName = annotation.name();
				if (opName.equals("")) {
					opName = method.getName();
				}
				String opDescription = annotation.description();
				Class returnType = method.getReturnType();
				List params = new ArrayList<>();
				Class[] paramTypes = method.getParameterTypes();
				Annotation[][] paramAnnotations = method.getParameterAnnotations();

				assert paramAnnotations.length == paramTypes.length;

				for (int i = 0; i < paramTypes.length; i++) {
					String paramName = String.format("arg%d", i);
					Class paramType = paramTypes[i];
					JmxParameter nameAnnotation = findJmxNamedParameterAnnotation(paramAnnotations[i]);
					if (nameAnnotation != null) {
						paramName = nameAnnotation.value();
					}
					MBeanParameterInfo paramInfo = new MBeanParameterInfo(paramName, paramType.getName(), "");
					params.add(paramInfo);
				}
				MBeanParameterInfo[] paramsArray = params.toArray(new MBeanParameterInfo[0]);
				MBeanOperationInfo operationInfo = new MBeanOperationInfo(
						opName, opDescription, paramsArray, returnType.getName(), MBeanOperationInfo.ACTION);
				operations.add(operationInfo);
			}
		}

		return operations.toArray(new MBeanOperationInfo[0]);
	}

	@Nullable
	private static JmxParameter findJmxNamedParameterAnnotation(Annotation[] annotations) {
		for (Annotation annotation : annotations) {
			if (annotation.annotationType().equals(JmxParameter.class)) {
				return (JmxParameter) annotation;
			}
		}
		return null;
	}
	// endregion

	// region jmx operations fetching
	private static Map fetchOpkeyToMethod(Class mbeanClass) {
		Map opkeyToMethod = new HashMap<>();
		Method[] methods = mbeanClass.getMethods();
		for (Method method : methods) {
			if (method.isAnnotationPresent(JmxOperation.class)) {
				JmxOperation annotation = method.getAnnotation(JmxOperation.class);
				String opName = annotation.name();
				if (opName.equals("")) {
					opName = method.getName();
				}
				Class[] paramTypes = method.getParameterTypes();
				Annotation[][] paramAnnotations = method.getParameterAnnotations();

				assert paramAnnotations.length == paramTypes.length;

				String[] paramTypesNames = new String[paramTypes.length];
				for (int i = 0; i < paramTypes.length; i++) {
					paramTypesNames[i] = paramTypes[i].getName();
				}
				opkeyToMethod.put(new OperationKey(opName, paramTypesNames), method);
			}
		}
		return opkeyToMethod;
	}
	// endregion

	// region helper classes
	private static final class AttributeDescriptor {
		private final String name;
		private final Type type;
		private final Method getter;
		private final Method setter;

		public AttributeDescriptor(String name, Type type, @Nullable Method getter, @Nullable Method setter) {
			this.name = name;
			this.type = type;
			this.getter = getter;
			this.setter = setter;
		}

		public String getName() {
			return name;
		}

		public Type getType() {
			return type;
		}

		@Nullable
		public Method getGetter() {
			return getter;
		}

		@Nullable
		public Method getSetter() {
			return setter;
		}
	}

	private static final class OperationKey {
		private final String name;
		private final String[] argTypes;

		public OperationKey(@NotNull String name, @NotNull String[] argTypes) {
			this.name = name;
			this.argTypes = argTypes;
		}

		public String getName() {
			return name;
		}

		@Override
		public boolean equals(Object o) {
			if (this == o) return true;
			if (!(o instanceof OperationKey)) return false;

			OperationKey that = (OperationKey) o;

			if (!name.equals(that.name)) return false;

			return Arrays.equals(argTypes, that.argTypes);

		}

		@Override
		public int hashCode() {
			int result = name.hashCode();
			result = 31 * result + Arrays.hashCode(argTypes);
			return result;
		}
	}

	private static final class DynamicMBeanAggregator implements DynamicMBean {
		private final MBeanInfo mBeanInfo;
		private final List mbeanWrappers;
		private final List mbeans;
		private final AttributeNodeForPojo rootNode;
		private final Map opkeyToMethod;

		public DynamicMBeanAggregator(MBeanInfo mBeanInfo, List mbeanWrappers,
				AttributeNodeForPojo rootNode, Map opkeyToMethod) {
			this.mBeanInfo = mBeanInfo;
			this.mbeanWrappers = mbeanWrappers;

			List extractedMBeans = new ArrayList<>(mbeanWrappers.size());
			for (MBeanWrapper mbeanWrapper : mbeanWrappers) {
				extractedMBeans.add(mbeanWrapper.getMBean());
			}
			this.mbeans = extractedMBeans;

			this.rootNode = rootNode;
			this.opkeyToMethod = opkeyToMethod;
		}

		@Override
		public Object getAttribute(String attribute) throws MBeanException {
			Object value = rootNode.aggregateAttributes(singleton(attribute), mbeans).get(attribute);
			if (value instanceof Throwable) {
				propagate((Throwable) value);
			}
			return value;
		}

		@Override
		public void setAttribute(Attribute attribute)
				throws MBeanException {
			String attrName = attribute.getName();
			Object attrValue = attribute.getValue();

			CountDownLatch latch = new CountDownLatch(mbeanWrappers.size());
			Ref exceptionRef = new Ref<>();

			for (MBeanWrapper mbeanWrapper : mbeanWrappers) {
				Object mbean = mbeanWrapper.getMBean();
				mbeanWrapper.execute(() -> {
					try {
						rootNode.setAttribute(attrName, attrValue, singletonList(mbean));
						latch.countDown();
					} catch (Exception e) {
						exceptionRef.set(e);
						latch.countDown();
					}
				});
			}

			try {
				latch.await();
			} catch (InterruptedException e) {
				throw new MBeanException(e);
			}

			Exception e = exceptionRef.get();
			if (e != null) {
				Exception actualException = e;
				if (e instanceof SetterException) {
					SetterException setterException = (SetterException) e;
					actualException = setterException.getCausedException();
				}
				propagate(actualException);
			}
		}

		@Override
		public AttributeList getAttributes(@NotNull String[] attributes) {
			AttributeList attrList = new AttributeList();
			Set attrNames = new HashSet<>(Arrays.asList(attributes));
			try {
				Map aggregatedAttrs = rootNode.aggregateAttributes(attrNames, mbeans);
				for (String aggregatedAttrName : aggregatedAttrs.keySet()) {
					Object aggregatedValue = aggregatedAttrs.get(aggregatedAttrName);
					if (!(aggregatedValue instanceof Throwable)) {
						attrList.add(new Attribute(aggregatedAttrName, aggregatedValue));
					}
				}
			} catch (Exception e) {
				logger.error("Cannot get attributes: " + attrNames, e);
			}
			return attrList;
		}

		@Override
		public AttributeList setAttributes(AttributeList attributes) {
			AttributeList resultList = new AttributeList();
			for (Object attr : attributes) {
				Attribute attribute = (Attribute) attr;
				try {
					setAttribute(attribute);
					resultList.add(new Attribute(attribute.getName(), attribute.getValue()));
				} catch (MBeanException e) {
					logger.error("Cannot set attribute: " + attribute.getName(), e);
				}
			}
			return resultList;
		}

		@Nullable
		@Override
		public Object invoke(String actionName, Object[] params, String[] signature)
				throws MBeanException {

			Object[] args = nullToDefault(params, new Object[0]);
			String[] argTypes = nullToDefault(signature, new String[0]);
			OperationKey opkey = new OperationKey(actionName, argTypes);
			Method opMethod = opkeyToMethod.get(opkey);
			if (opMethod == null) {
				String operationName = prettyOperationName(actionName, argTypes);
				String errorMsg = "There is no operation \"" + operationName + "\"";
				throw new RuntimeOperationsException(new IllegalArgumentException("Operation not found"), errorMsg);
			}

			CountDownLatch latch = new CountDownLatch(mbeanWrappers.size());
			Ref lastValueRef = new Ref<>();
			Ref exceptionRef = new Ref<>();
			for (MBeanWrapper mbeanWrapper : mbeanWrappers) {
				Object mbean = mbeanWrapper.getMBean();
				mbeanWrapper.execute(() -> {
					try {
						Object result = opMethod.invoke(mbean, args);
						lastValueRef.set(result);
						latch.countDown();
					} catch (Exception e) {
						exceptionRef.set(e);
						latch.countDown();
					}
				});
			}

			try {
				latch.await();
			} catch (InterruptedException e) {
				throw new MBeanException(e);
			}

			Exception e = exceptionRef.get();
			if (e != null) {
				propagate(e);
			}

			// We don't know how to aggregate return values if there are several mbeans
			return mbeanWrappers.size() == 1 ? lastValueRef.get() : null;
		}

		private void propagate(Throwable e) throws MBeanException {
			if (e instanceof InvocationTargetException) {
				Throwable targetException = ((InvocationTargetException) e).getTargetException();

				if (targetException instanceof Exception) {
					throw new MBeanException((Exception) targetException);
				} else {
					throw new MBeanException(
							new Exception(format("Throwable of type \"%s\" and message \"%s\" " +
											"was thrown during method invocation",
									targetException.getClass().getName(), targetException.getMessage())
							)
					);
				}

			} else {
				if (e instanceof Exception) {
					throw new MBeanException((Exception) e);
				} else {
					throw new MBeanException(
							new Exception(format("Throwable of type \"%s\" and message \"%s\" " +
											"was thrown",
									e.getClass().getName(), e.getMessage())
							)
					);
				}
			}
		}

		private static String prettyOperationName(String name, String[] argTypes) {
			StringBuilder operationName = new StringBuilder(name).append('(');
			if (argTypes.length > 0) {
				for (int i = 0; i < argTypes.length - 1; i++) {
					operationName.append(argTypes[i]).append(", ");
				}
				operationName.append(argTypes[argTypes.length - 1]);
			}
			operationName.append(")");
			return operationName.toString();
		}

		@Override
		public MBeanInfo getMBeanInfo() {
			return mBeanInfo;
		}
	}

	private interface MBeanWrapper {
		void execute(Runnable command);

		Object getMBean();

		Eventloop getEventloop();
	}

	private static final class ConcurrentJmxMBeanWrapper implements MBeanWrapper {
		private final ConcurrentJmxMBean mbean;

		public ConcurrentJmxMBeanWrapper(ConcurrentJmxMBean mbean) {
			this.mbean = mbean;
		}

		@Override
		public void execute(Runnable command) {
			command.run();
		}

		@Override
		public Object getMBean() {
			return mbean;
		}

		@Nullable
		@Override
		public Eventloop getEventloop() {
			return null;
		}
	}

	private static final class EventloopJmxMBeanWrapper implements MBeanWrapper {
		private final EventloopJmxMBean mbean;

		public EventloopJmxMBeanWrapper(EventloopJmxMBean mbean) {
			this.mbean = mbean;
		}

		@Override
		public void execute(Runnable command) {
			mbean.getEventloop().execute(command);
		}

		@Override
		public Object getMBean() {
			return mbean;
		}

		@Override
		public Eventloop getEventloop() {
			return mbean.getEventloop();
		}
	}

	static class JmxCustomTypeAdapter {
		@Nullable
		public final Function from;
		public final Function to;

		public JmxCustomTypeAdapter(Function to, @Nullable Function from) {
			this.to = to;
			this.from = from;
		}

		public JmxCustomTypeAdapter(Function to) {
			this.to = to;
			this.from = null;
		}
	}
	// endregion
}