This is an automated email from the ASF dual-hosted git repository.
JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 61f2014ded8 Fix client manager metrics and JMX lifecycle races (#18681)
61f2014ded8 is described below
commit 61f2014ded8d887c76968a3c54e85eeaa8aa50b0
Author: Jackie Tien <[email protected]>
AuthorDate: Mon Sep 21 09:05:15 2026 +0800
Fix client manager metrics and JMX lifecycle races (#18681)
---
iotdb-core/metrics/core/pom.xml | 5 +
.../metrics/core/reporter/IoTDBJmxReporter.java | 115 ++--
.../iotdb/metrics/core/type/IoTDBAutoGauge.java | 5 +-
.../core/utils/IoTDBMetricObjNameFactory.java | 38 +-
.../metrics/core/utils/ObjectNameFactory.java | 45 ++
.../metrics/core/MetricManagerLifecycleTest.java | 339 ++++++++++++
.../core/reporter/IoTDBJmxReporterTest.java | 205 ++++++++
.../metrics/core/type/IoTDBAutoGaugeTest.java | 68 +++
.../core/utils/IoTDBMetricObjNameFactoryTest.java | 123 +++++
.../iotdb/metrics/AbstractMetricManager.java | 215 +++++---
.../apache/iotdb/commons/i18n/ClientMessages.java | 3 +
.../apache/iotdb/commons/i18n/ClientMessages.java | 3 +
.../apache/iotdb/commons/client/ClientManager.java | 19 +-
.../iotdb/commons/client/ClientManagerMetrics.java | 200 +++----
.../commons/client/ClientManagerMetricsTest.java | 580 +++++++++++++++++++++
.../service/metric/MetricReporterSwitchTest.java | 166 ++++++
16 files changed, 1887 insertions(+), 242 deletions(-)
diff --git a/iotdb-core/metrics/core/pom.xml b/iotdb-core/metrics/core/pom.xml
index 2fa80bf1b11..ac7f9ac32f9 100644
--- a/iotdb-core/metrics/core/pom.xml
+++ b/iotdb-core/metrics/core/pom.xml
@@ -46,6 +46,11 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git
a/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/reporter/IoTDBJmxReporter.java
b/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/reporter/IoTDBJmxReporter.java
index 372afdb36f6..cb750ac64a2 100644
---
a/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/reporter/IoTDBJmxReporter.java
+++
b/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/reporter/IoTDBJmxReporter.java
@@ -40,8 +40,9 @@ import javax.management.ObjectInstance;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
+import java.util.HashMap;
+import java.util.Iterator;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
public class IoTDBJmxReporter implements JmxReporter {
private static final Logger LOGGER =
LoggerFactory.getLogger(IoTDBJmxReporter.class);
@@ -55,37 +56,38 @@ public class IoTDBJmxReporter implements JmxReporter {
/** The objectNameFactory used to create objectName for metrics */
private final ObjectNameFactory objectNameFactory;
- /** The map that stores all registered metrics */
- private final Map<ObjectName, ObjectName> registered;
+ /** Registrations owned by this reporter, guarded by the map's monitor. */
+ private final Map<ObjectName, Registration> registered;
+
+ private boolean started;
/** The JMX MBeanServer */
private final MBeanServer mBeanServer;
- private void registerMBean(Object mBean, ObjectName objectName) throws
JMException {
- if (!mBeanServer.isRegistered(objectName)) {
- ObjectInstance objectInstance = mBeanServer.registerMBean(mBean,
objectName);
- if (objectInstance != null) {
- // the websphere mbeanserver rewrites the objectname to include
- // cell, node & server info
- // make sure we capture the new objectName for unregistration
- registered.put(objectName, objectInstance.getObjectName());
- } else {
- registered.put(objectName, objectName);
+ private void registerMBean(IMetric metric, ObjectName objectName) throws
JMException {
+ Registration previous = registered.get(objectName);
+ if (previous != null) {
+ if (previous.metric == metric &&
mBeanServer.isRegistered(previous.actualName)) {
+ return;
}
+ unregisterMBean(previous);
+ registered.remove(objectName);
+ }
+ if (!mBeanServer.isRegistered(objectName)) {
+ ObjectInstance objectInstance = mBeanServer.registerMBean(metric,
objectName);
+ // Some MBean servers rewrite ObjectNames. Keep the actual name together
with its owner.
+ registered.put(
+ objectName,
+ new Registration(
+ metric, objectInstance == null ? objectName :
objectInstance.getObjectName()));
}
}
- private void unregisterMBean(ObjectName originalObjectName)
- throws InstanceNotFoundException, MBeanRegistrationException {
- ObjectName storedObjectName = registered.remove(originalObjectName);
- if (storedObjectName != null) {
- if (mBeanServer.isRegistered(storedObjectName)) {
- mBeanServer.unregisterMBean(storedObjectName);
- }
- } else {
- if (mBeanServer.isRegistered(originalObjectName)) {
- mBeanServer.unregisterMBean(originalObjectName);
- }
+ private void unregisterMBean(Registration registration) throws
MBeanRegistrationException {
+ try {
+ mBeanServer.unregisterMBean(registration.actualName);
+ } catch (InstanceNotFoundException ignored) {
+ // An externally removed MBean is already unregistered.
}
}
@@ -94,8 +96,14 @@ public class IoTDBJmxReporter implements JmxReporter {
String metricName = metric.getClass().getSimpleName();
try {
final ObjectName objectName = createName(metricName, metricInfo);
- metric.setObjectName(objectName);
- registerMBean(metric, objectName);
+ synchronized (registered) {
+ // Ignore callbacks from a stopped reporter or a superseded registry
entry.
+ if (!started || metricManager.getAllMetrics().get(metricInfo) !=
metric) {
+ return;
+ }
+ metric.setObjectName(objectName);
+ registerMBean(metric, objectName);
+ }
} catch (Exception e) {
LOGGER.warn(MetricsCoreMessages.JMX_REGISTER_FAILED + metricName, e);
}
@@ -103,12 +111,20 @@ public class IoTDBJmxReporter implements JmxReporter {
@Override
public void unregisterMetric(IMetric metric, MetricInfo metricInfo) {
+ if (metric == null) {
+ return;
+ }
String metricName = metric.getClass().getSimpleName();
try {
final ObjectName objectName = createName(metricName, metricInfo);
- unregisterMBean(objectName);
- } catch (InstanceNotFoundException e) {
- LOGGER.debug(MetricsCoreMessages.JMX_UNREGISTER_FAILED, e);
+ synchronized (registered) {
+ Registration registration = registered.get(objectName);
+ // A delayed callback for an old metric must not delete its
replacement.
+ if (registration != null && registration.metric == metric) {
+ unregisterMBean(registration);
+ registered.remove(objectName);
+ }
+ }
} catch (MBeanRegistrationException e) {
LOGGER.warn(MetricsCoreMessages.JMX_UNREGISTER_FAILED, e);
}
@@ -116,37 +132,45 @@ public class IoTDBJmxReporter implements JmxReporter {
private ObjectName createName(String type, MetricInfo metricInfo) {
String name = metricInfo.getName();
- return objectNameFactory.createName(type, DOMAIN, name);
+ return objectNameFactory.createName(type, DOMAIN, name,
metricInfo.getTags());
}
- void unregisterAll() throws InstanceNotFoundException,
MBeanRegistrationException {
- for (ObjectName name : registered.keySet()) {
- unregisterMBean(name);
+ void unregisterAll() throws MBeanRegistrationException {
+ synchronized (registered) {
+ Iterator<Registration> iterator = registered.values().iterator();
+ while (iterator.hasNext()) {
+ unregisterMBean(iterator.next());
+ iterator.remove();
+ }
}
- // clear registered
- registered.clear();
}
- private IoTDBJmxReporter(
+ IoTDBJmxReporter(
AbstractMetricManager metricManager,
MBeanServer mBeanServer,
ObjectNameFactory objectNameFactory) {
this.metricManager = metricManager;
this.mBeanServer = mBeanServer;
this.objectNameFactory = objectNameFactory;
- this.registered = new ConcurrentHashMap<>();
+ this.registered = new HashMap<>();
}
@Override
public boolean start() {
try {
- if (!registered.isEmpty()) {
+ boolean alreadyStarted;
+ synchronized (registered) {
+ alreadyStarted = started;
+ started = true;
+ }
+ if (alreadyStarted) {
LOGGER.warn(MetricsCoreMessages.JMX_REPORTER_ALREADY_START);
return false;
}
// register all existed metrics into JmxReporter
metricManager.getAllMetrics().forEach((key, value) ->
registerMetric(value, key));
} catch (Exception e) {
+ stop();
LOGGER.warn(MetricsCoreMessages.JMX_REPORTER_START_FAILED, e);
return false;
}
@@ -157,7 +181,10 @@ public class IoTDBJmxReporter implements JmxReporter {
@Override
public boolean stop() {
try {
- unregisterAll();
+ synchronized (registered) {
+ started = false;
+ unregisterAll();
+ }
} catch (Exception e) {
LOGGER.warn(MetricsCoreMessages.JMX_REPORTER_STOP_FAILED, e);
return false;
@@ -171,6 +198,16 @@ public class IoTDBJmxReporter implements JmxReporter {
return ReporterType.JMX;
}
+ private static class Registration {
+ private final IMetric metric;
+ private final ObjectName actualName;
+
+ private Registration(IMetric metric, ObjectName actualName) {
+ this.metric = metric;
+ this.actualName = actualName;
+ }
+ }
+
private static class IoTDBJmxReporterHolder {
private static final IoTDBJmxReporter INSTANCE =
new IoTDBJmxReporter(
diff --git
a/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/type/IoTDBAutoGauge.java
b/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/type/IoTDBAutoGauge.java
index dc0a6df902c..9b6e98fc270 100644
---
a/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/type/IoTDBAutoGauge.java
+++
b/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/type/IoTDBAutoGauge.java
@@ -37,9 +37,10 @@ public class IoTDBAutoGauge<T> extends AbstractMetricMBean
@Override
public double getValue() {
- if (refObject.get() == null) {
+ T object = refObject.get();
+ if (object == null) {
return 0d;
}
- return mapper.applyAsDouble(refObject.get());
+ return mapper.applyAsDouble(object);
}
}
diff --git
a/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/utils/IoTDBMetricObjNameFactory.java
b/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/utils/IoTDBMetricObjNameFactory.java
index bcc5a31dcbf..b2c3b914d4a 100644
---
a/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/utils/IoTDBMetricObjNameFactory.java
+++
b/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/utils/IoTDBMetricObjNameFactory.java
@@ -30,7 +30,7 @@ import javax.management.ObjectName;
import java.util.Hashtable;
public class IoTDBMetricObjNameFactory implements ObjectNameFactory {
- private static final char[] QUOTABLE_CHARS = new char[] {',', '=', ':', '"'};
+ private static final char[] QUOTABLE_CHARS = new char[] {',', '=', ':', '"',
'\n', '*', '?'};
private static final Logger LOGGER =
LoggerFactory.getLogger(IoTDBMetricObjNameFactory.class);
private IoTDBMetricObjNameFactory() {
@@ -40,38 +40,14 @@ public class IoTDBMetricObjNameFactory implements
ObjectNameFactory {
@Override
public ObjectName createName(String type, String domain, String name) {
try {
- ObjectName objectName;
Hashtable<String, String> properties = new Hashtable<>();
-
- properties.put("name", name);
- properties.put("type", type);
- objectName = new ObjectName(domain, properties);
-
- /*
- * The only way we can find out if we need to quote the properties is by
- * checking an ObjectName that we've constructed.
- */
- if (objectName.isDomainPattern()) {
- domain = ObjectName.quote(domain);
- }
- if (objectName.isPropertyValuePattern("name")
- || shouldQuote(objectName.getKeyProperty("name"))) {
- properties.put("name", ObjectName.quote(name));
- }
- if (objectName.isPropertyValuePattern("type")
- || shouldQuote(objectName.getKeyProperty("type"))) {
- properties.put("type", ObjectName.quote(type));
- }
- objectName = new ObjectName(domain, properties);
-
- return objectName;
+ // Quote before constructing the name; falling back to a name-only MBean
loses its type.
+ properties.put("name", shouldQuote(name) ? ObjectName.quote(name) :
name);
+ properties.put("type", shouldQuote(type) ? ObjectName.quote(type) :
type);
+ return new ObjectName(domain, properties);
} catch (MalformedObjectNameException e) {
- try {
- return new ObjectName(domain, "name", ObjectName.quote(name));
- } catch (MalformedObjectNameException e1) {
- LOGGER.warn(MetricsCoreMessages.JMX_UNABLE_TO_REGISTER, type, name,
e1);
- throw new RuntimeException(e1);
- }
+ LOGGER.warn(MetricsCoreMessages.JMX_UNABLE_TO_REGISTER, type, name, e);
+ throw new IllegalArgumentException(e);
}
}
diff --git
a/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/utils/ObjectNameFactory.java
b/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/utils/ObjectNameFactory.java
index 1c019781300..96a3bb2eb9c 100644
---
a/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/utils/ObjectNameFactory.java
+++
b/iotdb-core/metrics/core/src/main/java/org/apache/iotdb/metrics/core/utils/ObjectNameFactory.java
@@ -19,8 +19,12 @@
package org.apache.iotdb.metrics.core.utils;
+import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
+import java.util.Hashtable;
+import java.util.Map;
+
public interface ObjectNameFactory {
/**
* Create objectName for a certain metric.
@@ -31,4 +35,45 @@ public interface ObjectNameFactory {
* @return metric's objectName
*/
public ObjectName createName(String type, String domain, String name);
+
+ /**
+ * Include all metric tags in the MBean identity. Tag keys use a separate
namespace and reversible
+ * escaping so they cannot overwrite the metric name/type or collide after
sanitization. Tag
+ * values are quoted to preserve literal wildcard characters. Untagged
metrics keep their existing
+ * names.
+ */
+ default ObjectName createName(String type, String domain, String name,
Map<String, String> tags) {
+ ObjectName base = createName(type, domain, name);
+ if (tags.isEmpty()) {
+ return base;
+ }
+ Hashtable<String, String> properties = base.getKeyPropertyList();
+ tags.forEach((key, value) -> properties.put(encodeTagKey(key),
ObjectName.quote(value)));
+ try {
+ return new ObjectName(base.getDomain(), properties);
+ } catch (MalformedObjectNameException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private static String encodeTagKey(String key) {
+ StringBuilder encoded = new StringBuilder("tag.");
+ for (int i = 0; i < key.length(); i++) {
+ char character = key.charAt(i);
+ if ((character >= 'a' && character <= 'z')
+ || (character >= 'A' && character <= 'Z')
+ || (character >= '0' && character <= '9')
+ || character == '_'
+ || character == '-'
+ || character == '.') {
+ encoded.append(character);
+ } else {
+ encoded.append('%');
+ for (int shift = 12; shift >= 0; shift -= 4) {
+ encoded.append(Character.forDigit((character >> shift) & 0xf, 16));
+ }
+ }
+ }
+ return encoded.toString();
+ }
}
diff --git
a/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/MetricManagerLifecycleTest.java
b/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/MetricManagerLifecycleTest.java
new file mode 100644
index 00000000000..c80478b1e01
--- /dev/null
+++
b/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/MetricManagerLifecycleTest.java
@@ -0,0 +1,339 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.iotdb.metrics.core;
+
+import org.apache.iotdb.metrics.core.type.IoTDBAutoGauge;
+import org.apache.iotdb.metrics.core.type.IoTDBCounter;
+import org.apache.iotdb.metrics.impl.DoNothingMetricManager;
+import org.apache.iotdb.metrics.reporter.JmxReporter;
+import org.apache.iotdb.metrics.type.AutoGauge;
+import org.apache.iotdb.metrics.type.Counter;
+import org.apache.iotdb.metrics.type.IMetric;
+import org.apache.iotdb.metrics.utils.MetricInfo;
+import org.apache.iotdb.metrics.utils.MetricLevel;
+import org.apache.iotdb.metrics.utils.MetricType;
+import org.apache.iotdb.metrics.utils.ReporterType;
+
+import org.junit.Test;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.ToDoubleFunction;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class MetricManagerLifecycleTest {
+ @Test
+ public void testResetWaitsForRemovalButCachedMetricsRemainAccessible()
throws Exception {
+ TestMetricManager manager = new TestMetricManager();
+ RecordingReporter reporter = new RecordingReporter(manager);
+ manager.setBindJmxReporter(reporter);
+ AtomicInteger value = new AtomicInteger(3);
+ AutoGauge closing =
+ manager.createAutoGauge("closing", MetricLevel.IMPORTANT, value,
AtomicInteger::get);
+ Object[] cached = cachedMetrics(manager);
+ CountDownLatch removed = new CountDownLatch(1);
+ CountDownLatch resume = new CountDownLatch(1);
+ Map<MetricInfo, IMetric> original =
+ new ConcurrentHashMap<MetricInfo, IMetric>(manager.getAllMetrics()) {
+ @Override
+ public IMetric remove(Object key) {
+ IMetric metric = super.remove(key);
+ if (metric != null && ((MetricInfo)
key).getName().equals("closing")) {
+ removed.countDown();
+ await(resume);
+ }
+ return metric;
+ }
+ };
+ manager.useRegistry(original);
+ ExecutorService executor = Executors.newFixedThreadPool(3);
+ AtomicReference<Thread> removingThread = new AtomicReference<>();
+ AtomicReference<Thread> resettingThread = new AtomicReference<>();
+ CountDownLatch resetting = new CountDownLatch(1);
+ try {
+ Future<?> removal =
+ executor.submit(
+ () -> {
+ removingThread.set(Thread.currentThread());
+ manager.remove(MetricType.AUTO_GAUGE, "closing");
+ });
+ assertTrue(removed.await(5, TimeUnit.SECONDS));
+ Future<Counter> reset =
+ executor.submit(
+ () -> {
+ resettingThread.set(Thread.currentThread());
+ resetting.countDown();
+ manager.reset();
+ return manager.getOrCreateCounter(
+ "closing", MetricLevel.IMPORTANT, "generation", "new");
+ });
+ assertTrue(resetting.await(5, TimeUnit.SECONDS));
+ assertBlockedBy(resettingThread.get(), removingThread.get(), reset);
+ assertSame(original, manager.getAllMetrics());
+ Object[] actual = executor.submit(() -> cachedMetrics(manager)).get(5,
TimeUnit.SECONDS);
+ for (int i = 0; i < cached.length; i++) {
+ assertSame(cached[i], actual[i]);
+ }
+ ((Counter) actual[0]).inc();
+ assertEquals(1, ((Counter) cached[0]).getCount());
+ assertEquals(3, executor.submit(closing::getValue).get(5,
TimeUnit.SECONDS), 0);
+ resume.countDown();
+ removal.get(5, TimeUnit.SECONDS);
+ Counter replacement = reset.get(5, TimeUnit.SECONDS);
+ assertEquals(1, reporter.removals.get());
+ assertSame(closing, reporter.removed.get());
+ assertEquals(1, manager.getAllMetrics().size());
+ assertSame(
+ replacement,
+ manager.getOrCreateCounter("closing", MetricLevel.IMPORTANT,
"generation", "new"));
+ assertTrue(manager.hasMetadata("closing"));
+ } finally {
+ resume.countDown();
+ executor.shutdownNow();
+ assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
+ }
+ }
+
+ @Test
+ public void testConcurrentRemovalsNotifyOnlyOnce() throws Exception {
+ TestMetricManager manager = new TestMetricManager();
+ RecordingReporter reporter = new RecordingReporter(manager);
+ manager.setBindJmxReporter(reporter);
+ Counter counter = manager.getOrCreateCounter("counter",
MetricLevel.IMPORTANT);
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ CountDownLatch start = new CountDownLatch(1);
+ try {
+ List<Future<?>> removals = new ArrayList<>();
+ for (int i = 0; i < 2; i++) {
+ removals.add(
+ executor.submit(
+ () -> {
+ await(start);
+ manager.remove(MetricType.COUNTER, "counter");
+ }));
+ }
+ start.countDown();
+ for (Future<?> removal : removals) {
+ removal.get(5, TimeUnit.SECONDS);
+ }
+ manager.remove(MetricType.COUNTER, "counter");
+ assertEquals(1, reporter.removals.get());
+ assertSame(counter, reporter.removed.get());
+ } finally {
+ start.countDown();
+ executor.shutdownNow();
+ assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
+ }
+ }
+
+ @Test
+ public void testConcurrentCreationPublishesOneMetricBeforeReporting() throws
Exception {
+ TestMetricManager manager = new TestMetricManager();
+ RecordingReporter reporter = new RecordingReporter(manager);
+ manager.setBindJmxReporter(reporter);
+ ExecutorService executor = Executors.newFixedThreadPool(4);
+ CountDownLatch start = new CountDownLatch(1);
+ try {
+ List<Future<Counter>> counters = new ArrayList<>();
+ for (int i = 0; i < 4; i++) {
+ counters.add(
+ executor.submit(
+ () -> {
+ await(start);
+ return manager.getOrCreateCounter("counter",
MetricLevel.IMPORTANT);
+ }));
+ }
+ start.countDown();
+ Counter first = counters.get(0).get(5, TimeUnit.SECONDS);
+ for (Future<Counter> counter : counters) {
+ assertSame(first, counter.get(5, TimeUnit.SECONDS));
+ }
+ assertEquals(1, manager.counterCreations.get());
+ assertEquals(1, reporter.registrations.get());
+ } finally {
+ start.countDown();
+ executor.shutdownNow();
+ assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
+ }
+ }
+
+ @Test
+ public void testReporterCallbackDoesNotHoldRegistryLock() throws Exception {
+ TestMetricManager manager = new TestMetricManager();
+ RecordingReporter reporter = new RecordingReporter(manager);
+ manager.setBindJmxReporter(reporter);
+ Counter old = manager.getOrCreateCounter("counter", MetricLevel.IMPORTANT);
+ CountDownLatch callback = new CountDownLatch(1);
+ CountDownLatch resume = new CountDownLatch(1);
+ reporter.beforeRemoval =
+ () -> {
+ callback.countDown();
+ await(resume);
+ };
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ try {
+ Future<?> removal = executor.submit(() ->
manager.remove(MetricType.COUNTER, "counter"));
+ assertTrue(callback.await(5, TimeUnit.SECONDS));
+ // A reporter/MBean-server callback can run after reset without pinning
the registry lock.
+ Counter next =
+ executor
+ .submit(
+ () -> {
+ manager.reset();
+ return manager.getOrCreateCounter(
+ "counter", MetricLevel.IMPORTANT, "generation", "new");
+ })
+ .get(5, TimeUnit.SECONDS);
+ resume.countDown();
+ removal.get(5, TimeUnit.SECONDS);
+ assertSame(old, reporter.removed.get());
+ assertSame(
+ next, manager.getOrCreateCounter("counter", MetricLevel.IMPORTANT,
"generation", "new"));
+ assertTrue(manager.hasMetadata("counter"));
+ } finally {
+ resume.countDown();
+ executor.shutdownNow();
+ assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
+ }
+ }
+
+ private static Object[] cachedMetrics(TestMetricManager manager) {
+ return new Object[] {
+ manager.getOrCreateCounter("cached_counter", MetricLevel.IMPORTANT),
+ manager.getOrCreateGauge("cached_gauge", MetricLevel.IMPORTANT),
+ manager.getOrCreateRate("cached_rate", MetricLevel.IMPORTANT),
+ manager.getOrCreateHistogram("cached_histogram", MetricLevel.IMPORTANT),
+ manager.getOrCreateTimer("cached_timer", MetricLevel.IMPORTANT)
+ };
+ }
+
+ private static void await(CountDownLatch latch) {
+ try {
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError(e);
+ }
+ }
+
+ private static void assertBlockedBy(Thread thread, Thread owner, Future<?>
task) {
+ long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
+ while (!task.isDone() && System.nanoTime() < deadline) {
+ ThreadInfo info =
ManagementFactory.getThreadMXBean().getThreadInfo(thread.getId());
+ if (info != null
+ && info.getThreadState() == Thread.State.BLOCKED
+ && info.getLockOwnerId() == owner.getId()) {
+ return;
+ }
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
+ }
+ fail("Registry reset did not wait for the active removal");
+ }
+
+ private static class TestMetricManager extends DoNothingMetricManager {
+ private final AtomicInteger counterCreations = new AtomicInteger();
+
+ @Override
+ public boolean isEnableMetricInGivenLevel(MetricLevel level) {
+ return true;
+ }
+
+ @Override
+ public Counter createCounter() {
+ counterCreations.incrementAndGet();
+ return new IoTDBCounter();
+ }
+
+ @Override
+ public <T> AutoGauge createAutoGauge(T object, ToDoubleFunction<T> mapper)
{
+ return new IoTDBAutoGauge<>(object, mapper);
+ }
+
+ void useRegistry(Map<MetricInfo, IMetric> registry) {
+ metrics = registry;
+ }
+
+ void reset() {
+ stop();
+ }
+
+ boolean hasMetadata(String name) {
+ return nameToMetaInfo.containsKey(name);
+ }
+ }
+
+ private static class RecordingReporter implements JmxReporter {
+ private final TestMetricManager manager;
+ private final AtomicInteger registrations = new AtomicInteger();
+ private final AtomicInteger removals = new AtomicInteger();
+ private final AtomicReference<IMetric> removed = new AtomicReference<>();
+ private Runnable beforeRemoval = () -> {};
+
+ private RecordingReporter(TestMetricManager manager) {
+ this.manager = manager;
+ }
+
+ @Override
+ public void registerMetric(IMetric metric, MetricInfo info) {
+ assertSame(metric, manager.getAllMetrics().get(info));
+ registrations.incrementAndGet();
+ }
+
+ @Override
+ public void unregisterMetric(IMetric metric, MetricInfo info) {
+ assertNotNull(metric);
+ beforeRemoval.run();
+ removed.set(metric);
+ removals.incrementAndGet();
+ }
+
+ @Override
+ public boolean start() {
+ return true;
+ }
+
+ @Override
+ public boolean stop() {
+ return true;
+ }
+
+ @Override
+ public ReporterType getReporterType() {
+ return ReporterType.JMX;
+ }
+ }
+}
diff --git
a/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/reporter/IoTDBJmxReporterTest.java
b/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/reporter/IoTDBJmxReporterTest.java
new file mode 100644
index 00000000000..9bc6bd4fe09
--- /dev/null
+++
b/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/reporter/IoTDBJmxReporterTest.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.iotdb.metrics.core.reporter;
+
+import org.apache.iotdb.metrics.core.type.IoTDBAutoGauge;
+import org.apache.iotdb.metrics.core.type.IoTDBCounter;
+import org.apache.iotdb.metrics.core.utils.IoTDBMetricObjNameFactory;
+import org.apache.iotdb.metrics.impl.DoNothingMetricManager;
+import org.apache.iotdb.metrics.type.AutoGauge;
+import org.apache.iotdb.metrics.type.Counter;
+import org.apache.iotdb.metrics.utils.MetricInfo;
+import org.apache.iotdb.metrics.utils.MetricLevel;
+import org.apache.iotdb.metrics.utils.MetricType;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.management.MBeanServer;
+import javax.management.MBeanServerFactory;
+import javax.management.ObjectName;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.ToDoubleFunction;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+public class IoTDBJmxReporterTest {
+ private TestMetricManager manager;
+ private MBeanServer server;
+ private IoTDBJmxReporter reporter;
+ private final List<AtomicInteger> values = new ArrayList<>();
+
+ @Before
+ public void setUp() {
+ manager = new TestMetricManager();
+ server = MBeanServerFactory.newMBeanServer();
+ reporter = new IoTDBJmxReporter(manager, server,
IoTDBMetricObjNameFactory.getInstance());
+ manager.setBindJmxReporter(reporter);
+ assertTrue(reporter.start());
+ }
+
+ @After
+ public void tearDown() {
+ assertTrue(reporter.stop());
+ }
+
+ @Test
+ public void testTaggedMetricsAreIndependent() throws Exception {
+ AtomicInteger firstValue = new AtomicInteger(1);
+ AtomicInteger secondValue = new AtomicInteger(2);
+ IoTDBAutoGauge<?> first = gauge(firstValue, "first");
+ IoTDBAutoGauge<?> second = gauge(secondValue, "second");
+ assertNotEquals(first.objectName(), second.objectName());
+ assertEquals(1, (double) server.getAttribute(first.objectName(), "Value"),
0);
+ assertEquals(2, (double) server.getAttribute(second.objectName(),
"Value"), 0);
+
+ manager.remove(MetricType.AUTO_GAUGE, "client_manager", "name",
"num_active", "type", "second");
+ assertFalse(server.isRegistered(second.objectName()));
+ assertEquals(1, (double) server.getAttribute(first.objectName(), "Value"),
0);
+ }
+
+ @Test
+ public void testDelayedUnregisterPreservesReplacement() throws Exception {
+ AtomicInteger firstValue = new AtomicInteger(1);
+ AtomicInteger nextValue = new AtomicInteger(2);
+ IoTDBAutoGauge<?> first = gauge(firstValue, "pool");
+ IoTDBAutoGauge<?> next = gauge(nextValue, "pool");
+ assertEquals(first.objectName(), next.objectName());
+ reporter.unregisterMetric(first, info("pool"));
+ assertEquals(2, (double) server.getAttribute(next.objectName(), "Value"),
0);
+
+ assertTrue(reporter.stop());
+ assertTrue(reporter.start());
+ reporter.unregisterMetric(first, info("pool"));
+ assertEquals(2, (double) server.getAttribute(next.objectName(), "Value"),
0);
+ }
+
+ @Test
+ public void testUnregisterDoesNotRemoveUnownedMBean() throws Exception {
+ AtomicInteger value = new AtomicInteger(3);
+ values.add(value);
+ IoTDBAutoGauge<AtomicInteger> external = new IoTDBAutoGauge<>(value,
AtomicInteger::get);
+ ObjectName name =
+ IoTDBMetricObjNameFactory.getInstance()
+ .createName(
+ "IoTDBAutoGauge",
+ "org.apache.iotdb.metrics",
+ "client_manager",
+ info("pool").getTags());
+ server.registerMBean(external, name);
+ reporter.unregisterMetric(external, info("pool"));
+ assertEquals(3, (double) server.getAttribute(name, "Value"), 0);
+ assertTrue(reporter.stop());
+ assertTrue(server.isRegistered(name));
+ }
+
+ @Test
+ public void testRepeatedAndMissingUnregistration() {
+ AtomicInteger value = new AtomicInteger(1);
+ IoTDBAutoGauge<?> gauge = gauge(value, "pool");
+ manager.remove(MetricType.AUTO_GAUGE, "client_manager", "name",
"num_active", "type", "pool");
+ reporter.unregisterMetric(gauge, info("pool"));
+ reporter.unregisterMetric(null, info("pool"));
+ assertFalse(server.isRegistered(gauge.objectName()));
+ }
+
+ @Test
+ public void testCounterRegistration() throws Exception {
+ IoTDBCounter counter =
+ (IoTDBCounter)
+ manager.getOrCreateCounter("requests", MetricLevel.IMPORTANT,
"name", "client");
+ counter.inc(7);
+ assertEquals(7L, server.getAttribute(counter.objectName(), "Count"));
+ }
+
+ @Test
+ public void testDelayedRegistrationCannotResurrectRemovedMetric() {
+ IoTDBAutoGauge<?> gauge = gauge(new AtomicInteger(1), "pool");
+ manager.remove(MetricType.AUTO_GAUGE, "client_manager", "name",
"num_active", "type", "pool");
+ reporter.registerMetric(gauge, info("pool"));
+ assertFalse(server.isRegistered(gauge.objectName()));
+ }
+
+ @Test
+ public void testDelayedRegistrationCannotReplaceNewMetric() throws Exception
{
+ IoTDBAutoGauge<?> old = gauge(new AtomicInteger(1), "pool");
+ IoTDBAutoGauge<?> next = gauge(new AtomicInteger(2), "pool");
+ reporter.registerMetric(old, info("pool"));
+ assertEquals(2, (double) server.getAttribute(next.objectName(), "Value"),
0);
+ }
+
+ @Test
+ public void testRegistrationWhileStoppedIsDeferred() throws Exception {
+ assertTrue(reporter.stop());
+ IoTDBAutoGauge<?> gauge = gauge(new AtomicInteger(1), "pool");
+ ObjectName pattern = new ObjectName("org.apache.iotdb.metrics:*");
+ assertTrue(server.queryNames(pattern, null).isEmpty());
+ for (int i = 0; i < 3; i++) {
+ assertTrue(reporter.start());
+ assertEquals(1, server.queryNames(pattern, null).size());
+ assertEquals(1, (double) server.getAttribute(gauge.objectName(),
"Value"), 0);
+ assertTrue(reporter.stop());
+ assertTrue(server.queryNames(pattern, null).isEmpty());
+ }
+ }
+
+ private IoTDBAutoGauge<?> gauge(AtomicInteger value, String pool) {
+ values.add(value);
+ return (IoTDBAutoGauge<?>)
+ manager.createAutoGauge(
+ "client_manager",
+ MetricLevel.IMPORTANT,
+ value,
+ AtomicInteger::get,
+ "name",
+ "num_active",
+ "type",
+ pool);
+ }
+
+ private MetricInfo info(String pool) {
+ return new MetricInfo(
+ MetricType.AUTO_GAUGE, "client_manager", "name", "num_active", "type",
pool);
+ }
+
+ private static class TestMetricManager extends DoNothingMetricManager {
+ @Override
+ public boolean isEnableMetricInGivenLevel(MetricLevel level) {
+ return true;
+ }
+
+ @Override
+ public <T> AutoGauge createAutoGauge(T object, ToDoubleFunction<T> mapper)
{
+ return new IoTDBAutoGauge<>(object, mapper);
+ }
+
+ @Override
+ public Counter createCounter() {
+ return new IoTDBCounter();
+ }
+ }
+}
diff --git
a/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/type/IoTDBAutoGaugeTest.java
b/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/type/IoTDBAutoGaugeTest.java
new file mode 100644
index 00000000000..36131b3e498
--- /dev/null
+++
b/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/type/IoTDBAutoGaugeTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.iotdb.metrics.core.type;
+
+import org.junit.Test;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+
+public class IoTDBAutoGaugeTest {
+
+ @Test
+ public void testLiveAndClearedReferent() throws Exception {
+ AtomicInteger value = new AtomicInteger(7);
+ IoTDBAutoGauge<AtomicInteger> gauge = new IoTDBAutoGauge<>(value,
AtomicInteger::get);
+ assertEquals(7, gauge.getValue(), 0);
+ value.set(9);
+ assertEquals(9, gauge.getValue(), 0);
+
+ Field field = IoTDBAutoGauge.class.getDeclaredField("refObject");
+ field.setAccessible(true);
+ ((WeakReference<?>) field.get(gauge)).clear();
+ assertEquals(0, gauge.getValue(), 0);
+ }
+
+ @Test
+ public void testReferentRemainsAvailableForCurrentSample() throws Exception {
+ AtomicInteger value = new AtomicInteger(7);
+ IoTDBAutoGauge<AtomicInteger> gauge = new IoTDBAutoGauge<>(value,
AtomicInteger::get);
+ // Deterministically simulate collection between two weak-reference reads,
without relying on
+ // GC.
+ WeakReference<AtomicInteger> reference =
+ new WeakReference<AtomicInteger>(value) {
+ @Override
+ public AtomicInteger get() {
+ AtomicInteger referent = super.get();
+ clear();
+ return referent;
+ }
+ };
+ Field field = IoTDBAutoGauge.class.getDeclaredField("refObject");
+ field.setAccessible(true);
+ field.set(gauge, reference);
+
+ assertEquals(7, gauge.getValue(), 0);
+ assertEquals(0, gauge.getValue(), 0);
+ }
+}
diff --git
a/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/utils/IoTDBMetricObjNameFactoryTest.java
b/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/utils/IoTDBMetricObjNameFactoryTest.java
new file mode 100644
index 00000000000..9bd8f5195b9
--- /dev/null
+++
b/iotdb-core/metrics/core/src/test/java/org/apache/iotdb/metrics/core/utils/IoTDBMetricObjNameFactoryTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.iotdb.metrics.core.utils;
+
+import org.junit.Test;
+
+import javax.management.ObjectName;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+
+public class IoTDBMetricObjNameFactoryTest {
+ private final ObjectNameFactory factory =
IoTDBMetricObjNameFactory.getInstance();
+
+ @Test
+ public void testUntaggedNamesStayCompatible() throws Exception {
+ ObjectName expected = new
ObjectName("org.apache.iotdb.metrics:name=plain,type=IoTDBAutoGauge");
+ assertEquals(
+ expected, factory.createName("IoTDBAutoGauge",
"org.apache.iotdb.metrics", "plain"));
+ assertEquals(
+ expected,
+ factory.createName(
+ "IoTDBAutoGauge", "org.apache.iotdb.metrics", "plain",
Collections.emptyMap()));
+ }
+
+ @Test
+ public void testTagsCannotOverwriteMetricNameAndType() {
+ ObjectName name =
+ factory.createName(
+ "IoTDBAutoGauge",
+ "org.apache.iotdb.metrics",
+ "client_manager",
+ Map.of("name", "num_active", "type", "first", "tag.name",
"nested"));
+ assertEquals("client_manager", name.getKeyProperty("name"));
+ assertEquals("IoTDBAutoGauge", name.getKeyProperty("type"));
+ assertEquals("num_active",
ObjectName.unquote(name.getKeyProperty("tag.name")));
+ assertEquals("first", ObjectName.unquote(name.getKeyProperty("tag.type")));
+ assertEquals("nested",
ObjectName.unquote(name.getKeyProperty("tag.tag.name")));
+ assertEquals(5, name.getKeyPropertyList().size());
+ }
+
+ @Test
+ public void testTagOrderDoesNotChangeIdentity() {
+ Map<String, String> first = new LinkedHashMap<>();
+ first.put("name", "num_active");
+ first.put("type", "pool");
+ Map<String, String> second = new LinkedHashMap<>();
+ second.put("type", "pool");
+ second.put("name", "num_active");
+ ObjectName a =
+ factory.createName("IoTDBAutoGauge", "org.apache.iotdb.metrics",
"client_manager", first);
+ ObjectName b =
+ factory.createName("IoTDBAutoGauge", "org.apache.iotdb.metrics",
"client_manager", second);
+ assertEquals(a, b);
+ assertEquals(a.getCanonicalName(), b.getCanonicalName());
+ assertEquals(2, first.size());
+ second.put("type", "other");
+ assertNotEquals(
+ a,
+ factory.createName("IoTDBAutoGauge", "org.apache.iotdb.metrics",
"client_manager", second));
+ }
+
+ @Test
+ public void testEscapingIsLiteralAndCollisionFree() {
+ String value = "value,=:*?\"\\\n";
+ String[] keys = {"", "a:b", "a,b", "a=b", "a?b", "a*b", "a\nb", "a%b",
"a.b", "a%003ab", "标签"};
+ Set<ObjectName> names = new HashSet<>();
+ Map<String, String> tags = new LinkedHashMap<>();
+ for (String key : keys) {
+ ObjectName name =
+ factory.createName(
+ "IoTDBAutoGauge", "org.apache.iotdb.metrics", "client_manager",
Map.of(key, value));
+ assertFalse(name.isPattern());
+ names.add(name);
+ tags.put(key, value);
+ }
+ assertEquals(keys.length, names.size());
+ ObjectName all =
+ factory.createName("IoTDBAutoGauge", "org.apache.iotdb.metrics",
"client_manager", tags);
+ assertEquals(keys.length + 2, all.getKeyPropertyList().size());
+ all.getKeyPropertyList()
+ .forEach(
+ (key, actual) -> {
+ if (key.startsWith("tag.")) {
+ assertEquals(value, ObjectName.unquote(actual));
+ }
+ });
+ }
+
+ @Test
+ public void testSpecialMetricNamesRetainType() {
+ ObjectName name =
+ factory.createName(
+ "Gauge:*", "org.apache.iotdb.metrics", "metric,=:\"\n?",
Map.of("type", "pool"));
+ assertEquals("Gauge:*", ObjectName.unquote(name.getKeyProperty("type")));
+ assertEquals("metric,=:\"\n?",
ObjectName.unquote(name.getKeyProperty("name")));
+ assertFalse(name.isPattern());
+ }
+}
diff --git
a/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/AbstractMetricManager.java
b/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/AbstractMetricManager.java
index cf5df80f36a..ce9330ca5ef 100644
---
a/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/AbstractMetricManager.java
+++
b/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/AbstractMetricManager.java
@@ -21,7 +21,6 @@ package org.apache.iotdb.metrics;
import org.apache.iotdb.metrics.config.MetricConfig;
import org.apache.iotdb.metrics.config.MetricConfigDescriptor;
-import org.apache.iotdb.metrics.i18n.MetricsMessages;
import org.apache.iotdb.metrics.impl.DoNothingMetricManager;
import org.apache.iotdb.metrics.reporter.JmxReporter;
import org.apache.iotdb.metrics.type.AutoGauge;
@@ -53,13 +52,18 @@ public abstract class AbstractMetricManager {
private static final String ALREADY_EXISTS = " is already used for a
different type of name";
/** The map from metric name to metric metaInfo. */
- protected Map<String, MetricInfo.MetaInfo> nameToMetaInfo;
+ protected volatile Map<String, MetricInfo.MetaInfo> nameToMetaInfo;
/** The map from metricInfo to metric. */
- protected Map<MetricInfo, IMetric> metrics;
+ protected volatile Map<MetricInfo, IMetric> metrics;
/** The bind IoTDBJmxReporter */
- protected JmxReporter bindJmxReporter = null;
+ protected volatile JmxReporter bindJmxReporter = null;
+
+ /**
+ * Serializes creation/removal with registry resets; existing metric reads
do not take this lock.
+ */
+ private final Object metricLifecycleLock = new Object();
protected AbstractMetricManager() {
nameToMetaInfo = new ConcurrentHashMap<>();
@@ -72,10 +76,10 @@ public abstract class AbstractMetricManager {
* @param metric the created metric
* @param metricInfo the created metric info
*/
- private void notifyReporterOnAdd(IMetric metric, MetricInfo metricInfo) {
+ private static void notifyReporterOnAdd(
+ IMetric metric, MetricInfo metricInfo, JmxReporter reporter) {
// if the reporter type is JMX, register the new metric
- Optional.ofNullable(bindJmxReporter)
- .ifPresent(x -> bindJmxReporter.registerMetric(metric, metricInfo));
+ Optional.ofNullable(reporter).ifPresent(x -> x.registerMetric(metric,
metricInfo));
}
/**
@@ -84,10 +88,10 @@ public abstract class AbstractMetricManager {
* @param metric the removed metric
* @param metricInfo the removed metric info
*/
- private void notifyReporterOnRemove(IMetric metric, MetricInfo metricInfo) {
- // if the reporter type is JMX, unregister the new metric
- Optional.ofNullable(bindJmxReporter)
- .ifPresent(x -> bindJmxReporter.unregisterMetric(metric, metricInfo));
+ private static void notifyReporterOnRemove(
+ IMetric metric, MetricInfo metricInfo, JmxReporter reporter) {
+ // Use the captured reporter even if the current binding has changed.
+ Optional.ofNullable(reporter).ifPresent(x -> x.unregisterMetric(metric,
metricInfo));
}
/**
@@ -103,15 +107,23 @@ public abstract class AbstractMetricManager {
return DoNothingMetricManager.DO_NOTHING_COUNTER;
}
MetricInfo metricInfo = new MetricInfo(MetricType.COUNTER, name, tags);
- IMetric metric =
- metrics.computeIfAbsent(
- metricInfo,
- key -> {
- Counter counter = createCounter();
- nameToMetaInfo.put(name, metricInfo.getMetaInfo());
- notifyReporterOnAdd(counter, metricInfo);
- return counter;
- });
+ IMetric metric = metrics.get(metricInfo);
+ if (metric == null) {
+ JmxReporter reporter = null;
+ synchronized (metricLifecycleLock) {
+ if (invalid(metricLevel, name, tags)) {
+ return DoNothingMetricManager.DO_NOTHING_COUNTER;
+ }
+ metric = metrics.get(metricInfo);
+ if (metric == null) {
+ metric = createCounter();
+ nameToMetaInfo.put(name, metricInfo.getMetaInfo());
+ metrics.put(metricInfo, metric);
+ reporter = bindJmxReporter;
+ }
+ }
+ notifyReporterOnAdd(metric, metricInfo, reporter);
+ }
if (metric instanceof Counter) {
return (Counter) metric;
}
@@ -134,14 +146,24 @@ public abstract class AbstractMetricManager {
*/
public <T> AutoGauge createAutoGauge(
String name, MetricLevel metricLevel, T obj, ToDoubleFunction<T> mapper,
String... tags) {
- if (invalid(metricLevel, name, tags)) {
- return DoNothingMetricManager.DO_NOTHING_AUTO_GAUGE;
- }
MetricInfo metricInfo = new MetricInfo(MetricType.AUTO_GAUGE, name, tags);
- AutoGauge gauge = createAutoGauge(obj, mapper);
- nameToMetaInfo.put(name, metricInfo.getMetaInfo());
- metrics.put(metricInfo, gauge);
- notifyReporterOnAdd(gauge, metricInfo);
+ AutoGauge gauge;
+ IMetric previous;
+ JmxReporter reporter;
+ synchronized (metricLifecycleLock) {
+ if (invalid(metricLevel, name, tags)) {
+ return DoNothingMetricManager.DO_NOTHING_AUTO_GAUGE;
+ }
+ gauge = createAutoGauge(obj, mapper);
+ nameToMetaInfo.put(name, metricInfo.getMetaInfo());
+ previous = metrics.put(metricInfo, gauge);
+ reporter = bindJmxReporter;
+ }
+ // Publish before notifying, and never hold the registry lock across
MBean-server callbacks.
+ if (previous != null) {
+ notifyReporterOnRemove(previous, metricInfo, reporter);
+ }
+ notifyReporterOnAdd(gauge, metricInfo, reporter);
return gauge;
}
@@ -187,15 +209,23 @@ public abstract class AbstractMetricManager {
return DoNothingMetricManager.DO_NOTHING_GAUGE;
}
MetricInfo metricInfo = new MetricInfo(MetricType.GAUGE, name, tags);
- IMetric metric =
- metrics.computeIfAbsent(
- metricInfo,
- key -> {
- Gauge gauge = createGauge();
- nameToMetaInfo.put(name, metricInfo.getMetaInfo());
- notifyReporterOnAdd(gauge, metricInfo);
- return gauge;
- });
+ IMetric metric = metrics.get(metricInfo);
+ if (metric == null) {
+ JmxReporter reporter = null;
+ synchronized (metricLifecycleLock) {
+ if (invalid(metricLevel, name, tags)) {
+ return DoNothingMetricManager.DO_NOTHING_GAUGE;
+ }
+ metric = metrics.get(metricInfo);
+ if (metric == null) {
+ metric = createGauge();
+ nameToMetaInfo.put(name, metricInfo.getMetaInfo());
+ metrics.put(metricInfo, metric);
+ reporter = bindJmxReporter;
+ }
+ }
+ notifyReporterOnAdd(metric, metricInfo, reporter);
+ }
if (metric instanceof Gauge) {
return (Gauge) metric;
}
@@ -218,15 +248,23 @@ public abstract class AbstractMetricManager {
return DoNothingMetricManager.DO_NOTHING_RATE;
}
MetricInfo metricInfo = new MetricInfo(MetricType.RATE, name, tags);
- IMetric metric =
- metrics.computeIfAbsent(
- metricInfo,
- key -> {
- Rate rate = createRate();
- nameToMetaInfo.put(name, metricInfo.getMetaInfo());
- notifyReporterOnAdd(rate, metricInfo);
- return rate;
- });
+ IMetric metric = metrics.get(metricInfo);
+ if (metric == null) {
+ JmxReporter reporter = null;
+ synchronized (metricLifecycleLock) {
+ if (invalid(metricLevel, name, tags)) {
+ return DoNothingMetricManager.DO_NOTHING_RATE;
+ }
+ metric = metrics.get(metricInfo);
+ if (metric == null) {
+ metric = createRate();
+ nameToMetaInfo.put(name, metricInfo.getMetaInfo());
+ metrics.put(metricInfo, metric);
+ reporter = bindJmxReporter;
+ }
+ }
+ notifyReporterOnAdd(metric, metricInfo, reporter);
+ }
if (metric instanceof Rate) {
return (Rate) metric;
}
@@ -249,15 +287,23 @@ public abstract class AbstractMetricManager {
return DoNothingMetricManager.DO_NOTHING_HISTOGRAM;
}
MetricInfo metricInfo = new MetricInfo(MetricType.HISTOGRAM, name, tags);
- IMetric metric =
- metrics.computeIfAbsent(
- metricInfo,
- key -> {
- Histogram histogram = createHistogram();
- nameToMetaInfo.put(name, metricInfo.getMetaInfo());
- notifyReporterOnAdd(histogram, metricInfo);
- return histogram;
- });
+ IMetric metric = metrics.get(metricInfo);
+ if (metric == null) {
+ JmxReporter reporter = null;
+ synchronized (metricLifecycleLock) {
+ if (invalid(metricLevel, name, tags)) {
+ return DoNothingMetricManager.DO_NOTHING_HISTOGRAM;
+ }
+ metric = metrics.get(metricInfo);
+ if (metric == null) {
+ metric = createHistogram();
+ nameToMetaInfo.put(name, metricInfo.getMetaInfo());
+ metrics.put(metricInfo, metric);
+ reporter = bindJmxReporter;
+ }
+ }
+ notifyReporterOnAdd(metric, metricInfo, reporter);
+ }
if (metric instanceof Histogram) {
return (Histogram) metric;
}
@@ -280,15 +326,23 @@ public abstract class AbstractMetricManager {
return DoNothingMetricManager.DO_NOTHING_TIMER;
}
MetricInfo metricInfo = new MetricInfo(MetricType.TIMER, name, tags);
- IMetric metric =
- metrics.computeIfAbsent(
- metricInfo,
- key -> {
- Timer timer = createTimer();
- nameToMetaInfo.put(name, metricInfo.getMetaInfo());
- notifyReporterOnAdd(timer, metricInfo);
- return timer;
- });
+ IMetric metric = metrics.get(metricInfo);
+ if (metric == null) {
+ JmxReporter reporter = null;
+ synchronized (metricLifecycleLock) {
+ if (invalid(metricLevel, name, tags)) {
+ return DoNothingMetricManager.DO_NOTHING_TIMER;
+ }
+ metric = metrics.get(metricInfo);
+ if (metric == null) {
+ metric = createTimer();
+ nameToMetaInfo.put(name, metricInfo.getMetaInfo());
+ metrics.put(metricInfo, metric);
+ reporter = bindJmxReporter;
+ }
+ }
+ notifyReporterOnAdd(metric, metricInfo, reporter);
+ }
if (metric instanceof Timer) {
return (Timer) metric;
}
@@ -419,26 +473,27 @@ public abstract class AbstractMetricManager {
// region remove metric
/**
- * remove name.
+ * Remove a metric. Removing an already absent metric is a no-op.
*
* @param type the type of name
* @param name the name of name
* @param tags string pairs, like sg="ln" will be "sg", "ln"
- * @throws IllegalArgumentException when there has different type metric
with same name
*/
public void remove(MetricType type, String name, String... tags) {
MetricInfo metricInfo = new MetricInfo(type, name, tags);
- if (metrics.containsKey(metricInfo)) {
- if (type == metricInfo.getMetaInfo().getType()) {
- notifyReporterOnRemove(metrics.get(metricInfo), metricInfo);
- nameToMetaInfo.remove(metricInfo.getName());
- metrics.remove(metricInfo);
- removeMetric(type, metricInfo);
- } else {
- throw new IllegalArgumentException(
- metricInfo +
MetricsMessages.EXCEPTION_FAILED_REMOVE_BECAUSE_MISMATCH_TYPE_044E55F6);
+ IMetric removed;
+ JmxReporter reporter;
+ synchronized (metricLifecycleLock) {
+ removed = metrics.remove(metricInfo);
+ if (removed == null) {
+ return;
}
+ nameToMetaInfo.remove(metricInfo.getName());
+ removeMetric(type, metricInfo);
+ reporter = bindJmxReporter;
}
+ // The removed instance remains valid even if stop() replaces the registry
before this callback.
+ notifyReporterOnRemove(removed, metricInfo, reporter);
}
protected abstract void removeMetric(MetricType type, MetricInfo metricInfo);
@@ -451,14 +506,18 @@ public abstract class AbstractMetricManager {
}
public void setBindJmxReporter(JmxReporter reporter) {
- this.bindJmxReporter = reporter;
+ synchronized (metricLifecycleLock) {
+ this.bindJmxReporter = reporter;
+ }
}
/** Stop and clear metric manager. */
protected boolean stop() {
- metrics = new ConcurrentHashMap<>();
- nameToMetaInfo = new ConcurrentHashMap<>();
- return stopFramework();
+ synchronized (metricLifecycleLock) {
+ metrics = new ConcurrentHashMap<>();
+ nameToMetaInfo = new ConcurrentHashMap<>();
+ return stopFramework();
+ }
}
protected abstract boolean stopFramework();
diff --git
a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/ClientMessages.java
b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/ClientMessages.java
index a4a3a8e6feb..ac17ee9a866 100644
---
a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/ClientMessages.java
+++
b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/ClientMessages.java
@@ -30,6 +30,9 @@ public final class ClientMessages {
public static final String CLEAR_CLIENT_POOL_FAILED =
"Clear all client in pool for node {} failed.";
+ public static final String
LOG_FAILED_TO_UNREGISTER_CLIENT_POOL_METRICS_WHILE_CLOSING_CLIENT_MANAGER_101A9751
=
+ "Failed to unregister client pool metrics while closing client manager";
+
// ThriftClient
public static final String EXCEPTION_LEVEL_DETAIL =
"level-{} Exception class {}, message {}";
diff --git
a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/ClientMessages.java
b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/ClientMessages.java
index 1f4ad1070a8..06715a051d8 100644
---
a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/ClientMessages.java
+++
b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/ClientMessages.java
@@ -30,6 +30,9 @@ public final class ClientMessages {
public static final String CLEAR_CLIENT_POOL_FAILED =
"清除节点 {} 的所有连接池客户端失败。";
+ public static final String
LOG_FAILED_TO_UNREGISTER_CLIENT_POOL_METRICS_WHILE_CLOSING_CLIENT_MANAGER_101A9751
=
+ "关闭客户端管理器时,注销客户端连接池指标失败";
+
// ThriftClient
public static final String EXCEPTION_LEVEL_DETAIL =
"第 {} 层异常,类名 {},消息 {}";
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/ClientManager.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/ClientManager.java
index 147f3375683..336658fd5dd 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/ClientManager.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/ClientManager.java
@@ -125,10 +125,21 @@ public class ClientManager<K, V> implements
IClientManager<K, V> {
@Override
public void close() {
- pool.close();
- // we need to release tManagers for AsyncThriftClientFactory
- if (pool.getFactory() instanceof AsyncThriftClientFactory) {
- ((AsyncThriftClientFactory<K, V>) pool.getFactory()).close();
+ try {
+ pool.close();
+ // we need to release tManagers for AsyncThriftClientFactory
+ if (pool.getFactory() instanceof AsyncThriftClientFactory) {
+ ((AsyncThriftClientFactory<K, V>) pool.getFactory()).close();
+ }
+ } finally {
+ try {
+ ClientManagerMetrics.getInstance().unregisterClientManager(pool);
+ } catch (RuntimeException e) {
+ LOGGER.warn(
+ ClientMessages
+
.LOG_FAILED_TO_UNREGISTER_CLIENT_POOL_METRICS_WHILE_CLOSING_CLIENT_MANAGER_101A9751,
+ e);
+ }
}
}
}
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/ClientManagerMetrics.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/ClientManagerMetrics.java
index cabc917ddb5..b1bade78d81 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/ClientManagerMetrics.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/ClientManagerMetrics.java
@@ -29,6 +29,7 @@ import org.apache.iotdb.metrics.utils.MetricType;
import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.Map;
public class ClientManagerMetrics implements IMetricSet {
@@ -58,35 +59,50 @@ public class ClientManagerMetrics implements IMetricSet {
// empty constructor
}
- public void registerClientManager(String poolName, GenericKeyedObjectPool<?,
?> clientPool) {
- synchronized (this) {
- if (metricService == null) {
- poolMap.put(poolName, clientPool);
- } else {
- if (!poolMap.containsKey(poolName)) {
- poolMap.put(poolName, clientPool);
- createMetrics(poolName);
+ public synchronized void registerClientManager(
+ String poolName, GenericKeyedObjectPool<?, ?> clientPool) {
+ GenericKeyedObjectPool<?, ?> existingPool = poolMap.get(poolName);
+ if (metricService != null && existingPool != null &&
!existingPool.isClosed()) {
+ return;
+ }
+ if (metricService != null && existingPool != null) {
+ removeMetrics(metricService, poolName);
+ }
+ poolMap.put(poolName, clientPool);
+ if (metricService != null) {
+ createMetrics(poolName, clientPool);
+ }
+ }
+
+ public synchronized void unregisterClientManager(GenericKeyedObjectPool<?,
?> clientPool) {
+ Iterator<Map.Entry<String, GenericKeyedObjectPool<?, ?>>> iterator =
+ poolMap.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<String, GenericKeyedObjectPool<?, ?>> entry = iterator.next();
+ // A pool with the same name may have been registered while the old pool
was closing.
+ if (entry.getValue() == clientPool) {
+ iterator.remove();
+ if (metricService != null) {
+ removeMetrics(metricService, entry.getKey());
}
}
}
}
@Override
- public void bindTo(AbstractMetricService metricService) {
+ public synchronized void bindTo(AbstractMetricService metricService) {
this.metricService = metricService;
- synchronized (this) {
- for (String poolName : poolMap.keySet()) {
- createMetrics(poolName);
- }
+ for (Map.Entry<String, GenericKeyedObjectPool<?, ?>> entry :
poolMap.entrySet()) {
+ createMetrics(entry.getKey(), entry.getValue());
}
}
- private void createMetrics(String poolName) {
+ private void createMetrics(String poolName, GenericKeyedObjectPool<?, ?>
clientPool) {
metricService.createAutoGauge(
Metric.CLIENT_MANAGER.toString(),
MetricLevel.IMPORTANT,
- poolMap,
- map -> poolMap.get(poolName).getNumActive(),
+ clientPool,
+ GenericKeyedObjectPool::getNumActive,
Tag.NAME.toString(),
CLIENT_MANAGER_NUM_ACTIVE,
Tag.TYPE.toString(),
@@ -94,8 +110,8 @@ public class ClientManagerMetrics implements IMetricSet {
metricService.createAutoGauge(
Metric.CLIENT_MANAGER.toString(),
MetricLevel.IMPORTANT,
- poolMap,
- map -> poolMap.get(poolName).getNumIdle(),
+ clientPool,
+ GenericKeyedObjectPool::getNumIdle,
Tag.NAME.toString(),
CLIENT_MANAGER_NUM_IDLE,
Tag.TYPE.toString(),
@@ -103,8 +119,8 @@ public class ClientManagerMetrics implements IMetricSet {
metricService.createAutoGauge(
Metric.CLIENT_MANAGER.toString(),
MetricLevel.IMPORTANT,
- poolMap,
- map -> poolMap.get(poolName).getBorrowedCount(),
+ clientPool,
+ GenericKeyedObjectPool::getBorrowedCount,
Tag.NAME.toString(),
CLIENT_MANAGER_BORROWED_COUNT,
Tag.TYPE.toString(),
@@ -112,8 +128,8 @@ public class ClientManagerMetrics implements IMetricSet {
metricService.createAutoGauge(
Metric.CLIENT_MANAGER.toString(),
MetricLevel.IMPORTANT,
- poolMap,
- map -> poolMap.get(poolName).getCreatedCount(),
+ clientPool,
+ GenericKeyedObjectPool::getCreatedCount,
Tag.NAME.toString(),
CLIENT_MANAGER_CREATED_COUNT,
Tag.TYPE.toString(),
@@ -121,8 +137,8 @@ public class ClientManagerMetrics implements IMetricSet {
metricService.createAutoGauge(
Metric.CLIENT_MANAGER.toString(),
MetricLevel.IMPORTANT,
- poolMap,
- map -> poolMap.get(poolName).getDestroyedCount(),
+ clientPool,
+ GenericKeyedObjectPool::getDestroyedCount,
Tag.NAME.toString(),
CLIENT_MANAGER_DESTROYED_COUNT,
Tag.TYPE.toString(),
@@ -130,8 +146,8 @@ public class ClientManagerMetrics implements IMetricSet {
metricService.createAutoGauge(
Metric.CLIENT_MANAGER.toString(),
MetricLevel.IMPORTANT,
- poolMap,
- map -> poolMap.get(poolName).getMeanActiveTimeMillis(),
+ clientPool,
+ GenericKeyedObjectPool::getMeanActiveTimeMillis,
Tag.NAME.toString(),
MEAN_ACTIVE_TIME_MILLIS,
Tag.TYPE.toString(),
@@ -139,8 +155,8 @@ public class ClientManagerMetrics implements IMetricSet {
metricService.createAutoGauge(
Metric.CLIENT_MANAGER.toString(),
MetricLevel.IMPORTANT,
- poolMap,
- map -> poolMap.get(poolName).getMeanBorrowWaitTimeMillis(),
+ clientPool,
+ GenericKeyedObjectPool::getMeanBorrowWaitTimeMillis,
Tag.NAME.toString(),
MEAN_BORROW_WAIT_TIME_MILLIS,
Tag.TYPE.toString(),
@@ -148,8 +164,8 @@ public class ClientManagerMetrics implements IMetricSet {
metricService.createAutoGauge(
Metric.CLIENT_MANAGER.toString(),
MetricLevel.IMPORTANT,
- poolMap,
- map -> poolMap.get(poolName).getMeanIdleTimeMillis(),
+ clientPool,
+ GenericKeyedObjectPool::getMeanIdleTimeMillis,
Tag.NAME.toString(),
MEAN_IDLE_TIME_MILLIS,
Tag.TYPE.toString(),
@@ -157,65 +173,73 @@ public class ClientManagerMetrics implements IMetricSet {
}
@Override
- public void unbindFrom(AbstractMetricService metricService) {
+ public synchronized void unbindFrom(AbstractMetricService metricService) {
+ if (this.metricService != metricService) {
+ return;
+ }
+ this.metricService = null;
+ // Keep live pools registered so a metric service restart can bind them
again.
for (String poolName : poolMap.keySet()) {
- metricService.remove(
- MetricType.GAUGE,
- Metric.CLIENT_MANAGER.toString(),
- Tag.NAME.toString(),
- CLIENT_MANAGER_NUM_ACTIVE,
- Tag.TYPE.toString(),
- poolName);
- metricService.remove(
- MetricType.GAUGE,
- Metric.CLIENT_MANAGER.toString(),
- Tag.NAME.toString(),
- CLIENT_MANAGER_NUM_IDLE,
- Tag.TYPE.toString(),
- poolName);
- metricService.remove(
- MetricType.GAUGE,
- Metric.CLIENT_MANAGER.toString(),
- Tag.NAME.toString(),
- CLIENT_MANAGER_BORROWED_COUNT,
- Tag.TYPE.toString(),
- poolName);
- metricService.remove(
- MetricType.GAUGE,
- Metric.CLIENT_MANAGER.toString(),
- Tag.NAME.toString(),
- CLIENT_MANAGER_CREATED_COUNT,
- Tag.TYPE.toString(),
- poolName);
- metricService.remove(
- MetricType.GAUGE,
- Metric.CLIENT_MANAGER.toString(),
- Tag.NAME.toString(),
- CLIENT_MANAGER_DESTROYED_COUNT,
- Tag.TYPE.toString(),
- poolName);
- metricService.remove(
- MetricType.GAUGE,
- Metric.CLIENT_MANAGER.toString(),
- Tag.NAME.toString(),
- MEAN_ACTIVE_TIME_MILLIS,
- Tag.TYPE.toString(),
- poolName);
- metricService.remove(
- MetricType.GAUGE,
- Metric.CLIENT_MANAGER.toString(),
- Tag.NAME.toString(),
- MEAN_BORROW_WAIT_TIME_MILLIS,
- Tag.TYPE.toString(),
- poolName);
- metricService.remove(
- MetricType.GAUGE,
- Metric.CLIENT_MANAGER.toString(),
- Tag.NAME.toString(),
- MEAN_IDLE_TIME_MILLIS,
- Tag.TYPE.toString(),
- poolName);
+ removeMetrics(metricService, poolName);
}
- poolMap.clear();
+ }
+
+ private void removeMetrics(AbstractMetricService metricService, String
poolName) {
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.CLIENT_MANAGER.toString(),
+ Tag.NAME.toString(),
+ CLIENT_MANAGER_NUM_ACTIVE,
+ Tag.TYPE.toString(),
+ poolName);
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.CLIENT_MANAGER.toString(),
+ Tag.NAME.toString(),
+ CLIENT_MANAGER_NUM_IDLE,
+ Tag.TYPE.toString(),
+ poolName);
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.CLIENT_MANAGER.toString(),
+ Tag.NAME.toString(),
+ CLIENT_MANAGER_BORROWED_COUNT,
+ Tag.TYPE.toString(),
+ poolName);
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.CLIENT_MANAGER.toString(),
+ Tag.NAME.toString(),
+ CLIENT_MANAGER_CREATED_COUNT,
+ Tag.TYPE.toString(),
+ poolName);
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.CLIENT_MANAGER.toString(),
+ Tag.NAME.toString(),
+ CLIENT_MANAGER_DESTROYED_COUNT,
+ Tag.TYPE.toString(),
+ poolName);
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.CLIENT_MANAGER.toString(),
+ Tag.NAME.toString(),
+ MEAN_ACTIVE_TIME_MILLIS,
+ Tag.TYPE.toString(),
+ poolName);
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.CLIENT_MANAGER.toString(),
+ Tag.NAME.toString(),
+ MEAN_BORROW_WAIT_TIME_MILLIS,
+ Tag.TYPE.toString(),
+ poolName);
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.CLIENT_MANAGER.toString(),
+ Tag.NAME.toString(),
+ MEAN_IDLE_TIME_MILLIS,
+ Tag.TYPE.toString(),
+ poolName);
}
}
diff --git
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/client/ClientManagerMetricsTest.java
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/client/ClientManagerMetricsTest.java
new file mode 100644
index 00000000000..e3eda335b16
--- /dev/null
+++
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/client/ClientManagerMetricsTest.java
@@ -0,0 +1,580 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.iotdb.commons.client;
+
+import org.apache.iotdb.commons.service.metric.MetricService;
+import org.apache.iotdb.commons.service.metric.enums.Metric;
+import org.apache.iotdb.commons.service.metric.enums.Tag;
+import org.apache.iotdb.metrics.AbstractMetricManager;
+import org.apache.iotdb.metrics.DoNothingMetricService;
+import org.apache.iotdb.metrics.config.MetricConfig;
+import org.apache.iotdb.metrics.config.MetricConfigDescriptor;
+import org.apache.iotdb.metrics.core.IoTDBMetricManager;
+import org.apache.iotdb.metrics.core.reporter.IoTDBJmxReporter;
+import org.apache.iotdb.metrics.core.type.IoTDBAutoGauge;
+import org.apache.iotdb.metrics.type.AutoGauge;
+import org.apache.iotdb.metrics.type.IMetric;
+import org.apache.iotdb.metrics.utils.MetricInfo;
+import org.apache.iotdb.metrics.utils.MetricLevel;
+import org.apache.iotdb.metrics.utils.MetricType;
+
+import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
+import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+public class ClientManagerMetricsTest {
+
+ private final ClientManagerMetrics metrics =
ClientManagerMetrics.getInstance();
+ private final List<ClientManager<String, Object>> managers = new
ArrayList<>();
+ private final MetricConfig config =
MetricConfigDescriptor.getInstance().getMetricConfig();
+ private TestMetricService service;
+ private MetricLevel originalLevel;
+ private String originalReporters;
+
+ @Before
+ public void setUp() {
+ originalLevel = config.getMetricLevel();
+ originalReporters =
+
config.getMetricReporterList().stream().map(Enum::name).collect(Collectors.joining(","));
+ config.setMetricLevel(MetricLevel.IMPORTANT);
+ config.setMetricReporterList("");
+ service = new TestMetricService();
+ service.startService();
+ service.addMetricSet(metrics);
+ }
+
+ @After
+ public void tearDown() {
+ managers.forEach(ClientManager::close);
+ service.removeMetricSet(metrics);
+ service.stopService();
+ config.setMetricLevel(originalLevel);
+ config.setMetricReporterList(originalReporters);
+ }
+
+ @Test
+ public void testInFlightGaugesAndRepeatedRebind() throws Exception {
+ ClientManager<String, Object> manager = createManager("pool");
+ Object client = manager.borrowClient("node");
+ manager.returnClient("node", client);
+ manager.borrowClient("node");
+ manager.getPool().addObject("node");
+ Map<MetricInfo, IMetric> inFlight = new HashMap<>(service.getAllMetrics());
+ assertEquals(8, inFlight.size());
+
+ for (int i = 0; i < 3; i++) {
+ service.stopService();
+ assertTrue(service.getAllMetrics().isEmpty());
+ assertMetrics(inFlight, "pool", manager.getPool());
+ service.startService();
+ assertEquals(8, service.getAllMetrics().size());
+ assertMetrics(service.getAllMetrics(), "pool", manager.getPool());
+ }
+
+ manager.borrowClient("node");
+ assertMetrics(inFlight, "pool", manager.getPool());
+ }
+
+ @Test
+ public void testRegistrationWhileUnbound() {
+ ClientManager<String, Object> first = createManager("first");
+ service.removeMetricSet(metrics);
+ ClientManager<String, Object> second = createManager("second");
+ assertTrue(service.getAllMetrics().isEmpty());
+
+ service.addMetricSet(metrics);
+ assertEquals(16, service.getAllMetrics().size());
+ assertMetrics(service.getAllMetrics(), "first", first.getPool());
+ assertMetrics(service.getAllMetrics(), "second", second.getPool());
+ }
+
+ @Test
+ public void testMetricServiceRestartAndLevelChanges() throws Exception {
+ ClientManager<String, Object> manager = createManager("pool");
+ manager.borrowClient("node");
+ service.removeMetricSet(metrics);
+ MetricService metricService = MetricService.getInstance();
+ metricService.startService();
+ try {
+ metricService.addMetricSet(metrics);
+ for (MetricLevel level :
+ new MetricLevel[] {MetricLevel.ALL, MetricLevel.CORE,
MetricLevel.IMPORTANT}) {
+ config.setMetricLevel(level);
+ metricService.restartService();
+ if (level == MetricLevel.CORE) {
+ assertTrue(metricService.getAllMetrics().isEmpty());
+ } else {
+ assertEquals(8, metricService.getAllMetrics().size());
+ assertMetrics(metricService.getAllMetrics(), "pool",
manager.getPool());
+ }
+ }
+ } finally {
+ metricService.removeMetricSet(metrics);
+ metricService.stopService();
+ }
+ }
+
+ @Test
+ public void testClosedPoolsAreNotRetainedAcrossRebinds() {
+ for (int i = 0; i < 10; i++) {
+ ClientManager<String, Object> manager = createManager("pool" + i);
+ assertEquals(8, service.getAllMetrics().size());
+ manager.close();
+ assertTrue(service.getAllMetrics().isEmpty());
+ service.stopService();
+ service.startService();
+ assertTrue(service.getAllMetrics().isEmpty());
+ }
+ }
+
+ @Test
+ public void testCloseWhileUnboundRemovesOnlyClosedPool() {
+ ClientManager<String, Object> first = createManager("first");
+ ClientManager<String, Object> second = createManager("second");
+ service.removeMetricSet(metrics);
+ first.close();
+ service.addMetricSet(metrics);
+
+ assertEquals(8, service.getAllMetrics().size());
+ assertMetrics(service.getAllMetrics(), "second", second.getPool());
+ }
+
+ @Test
+ public void testClosingSupersededPoolDoesNotRemoveReplacement() throws
Exception {
+ ClientManager<String, Object> first = createManager("pool");
+ first.borrowClient("node");
+ Map<MetricInfo, IMetric> inFlight = new HashMap<>(service.getAllMetrics());
+ service.removeMetricSet(metrics);
+ ClientManager<String, Object> replacement = createManager("pool");
+ replacement.borrowClient("node");
+ replacement.borrowClient("node");
+ service.addMetricSet(metrics);
+
+ assertMetrics(inFlight, "pool", first.getPool());
+ first.close();
+ assertEquals(8, service.getAllMetrics().size());
+ assertMetrics(service.getAllMetrics(), "pool", replacement.getPool());
+ assertMetrics(inFlight, "pool", first.getPool());
+ }
+
+ @Test
+ public void testClosingUnregisteredDuplicateDoesNotRemoveOriginal() throws
Exception {
+ ClientManager<String, Object> first = createManager("pool");
+ first.borrowClient("node");
+ ClientManager<String, Object> duplicate = createManager("pool");
+ duplicate.close();
+
+ assertEquals(8, service.getAllMetrics().size());
+ assertMetrics(service.getAllMetrics(), "pool", first.getPool());
+ }
+
+ @Test
+ public void testClosedPoolCanBeReplacedBeforeUnregistration() throws
Exception {
+ IoTDBJmxReporter reporter = IoTDBJmxReporter.getInstance();
+ assertTrue(reporter.start());
+ service.getMetricManager().setBindJmxReporter(reporter);
+ try {
+ ClientManager<String, Object> first = createManager("pool");
+ first.borrowClient("node");
+ ObjectName objectName =
+ ((IoTDBAutoGauge<?>)
+ service.getAutoGauge(
+ Metric.CLIENT_MANAGER.toString(),
+ MetricLevel.IMPORTANT,
+ Tag.NAME.toString(),
+ "client_manager_num_active",
+ Tag.TYPE.toString(),
+ "pool"))
+ .objectName();
+ MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
+ assertEquals(1, (double) mBeanServer.getAttribute(objectName, "Value"),
0);
+ // The pool is closed, but its owner's close has not reached metric
unregistration yet.
+ first.getPool().close();
+ ClientManager<String, Object> replacement = createManager("pool");
+ replacement.borrowClient("node");
+ replacement.borrowClient("node");
+ first.close();
+
+ assertEquals(8, service.getAllMetrics().size());
+ assertMetrics(service.getAllMetrics(), "pool", replacement.getPool());
+ assertEquals(2, (double) mBeanServer.getAttribute(objectName, "Value"),
0);
+ } finally {
+ service.getMetricManager().setBindJmxReporter(null);
+ reporter.stop();
+ }
+ }
+
+ @Test
+ public void testClosingAnotherPoolPreservesAllLivePoolMBeans() throws
Exception {
+ IoTDBJmxReporter reporter = IoTDBJmxReporter.getInstance();
+ assertTrue(reporter.start());
+ service.getMetricManager().setBindJmxReporter(reporter);
+ MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+ ObjectName pattern =
+ new
ObjectName("org.apache.iotdb.metrics:name=client_manager,type=IoTDBAutoGauge,*");
+ try {
+ ClientManager<String, Object> first = createManager("first");
+ first.borrowClient("node");
+ ClientManager<String, Object> second = createManager("second");
+ second.borrowClient("node");
+ second.borrowClient("node");
+ assertEquals(16, server.queryNames(pattern, null).size());
+ assertJmxMetrics(server);
+
+ second.close();
+ assertEquals(8, server.queryNames(pattern, null).size());
+ assertMetrics(service.getAllMetrics(), "first", first.getPool());
+ assertJmxMetrics(server);
+
+ ClientManager<String, Object> replacement = createManager("second");
+ replacement.borrowClient("node");
+ second.close();
+ assertEquals(16, server.queryNames(pattern, null).size());
+ assertJmxMetrics(server);
+ replacement.close();
+ assertEquals(8, server.queryNames(pattern, null).size());
+ assertJmxMetrics(server);
+ first.close();
+ assertTrue(server.queryNames(pattern, null).isEmpty());
+ } finally {
+ service.getMetricManager().setBindJmxReporter(null);
+ reporter.stop();
+ }
+ }
+
+ private void assertJmxMetrics(MBeanServer server) throws Exception {
+ assertJmxMetrics(server, service.getAllMetrics());
+ }
+
+ private void assertJmxMetrics(MBeanServer server, Map<MetricInfo, IMetric>
allMetrics)
+ throws Exception {
+ for (IMetric metric : allMetrics.values()) {
+ IoTDBAutoGauge<?> gauge = (IoTDBAutoGauge<?>) metric;
+ assertEquals(gauge.getValue(), (double)
server.getAttribute(gauge.objectName(), "Value"), 0);
+ }
+ }
+
+ @Test
+ public void testCloseAndCoreRestartAreSerialized() throws Exception {
+ service.removeMetricSet(metrics);
+ config.setMetricReporterList("JMX");
+ MetricService realService = MetricService.getInstance();
+ realService.startService();
+ CountDownLatch removed = new CountDownLatch(1);
+ CountDownLatch resume = new CountDownLatch(1);
+ ExecutorService executor = Executors.newFixedThreadPool(3);
+ try {
+ realService.addMetricSet(metrics);
+ ClientManager<String, Object> closing = createManager("closing");
+ closing.borrowClient("node");
+ ClientManager<String, Object> live = createManager("live");
+ live.borrowClient("node");
+ AutoGauge liveGauge =
+ realService.getAutoGauge(
+ Metric.CLIENT_MANAGER.toString(),
+ MetricLevel.IMPORTANT,
+ Tag.NAME.toString(),
+ "client_manager_num_active",
+ Tag.TYPE.toString(),
+ "live");
+ AbstractMetricManager manager = realService.getMetricManager();
+ AtomicBoolean pauseOnce = new AtomicBoolean(true);
+ Map<MetricInfo, IMetric> registry =
+ new ConcurrentHashMap<MetricInfo, IMetric>(manager.getAllMetrics()) {
+ @Override
+ public IMetric remove(Object key) {
+ IMetric metric = super.remove(key);
+ if (metric != null
+ && "closing".equals(((MetricInfo)
key).getTags().get(Tag.TYPE.toString()))
+ && pauseOnce.compareAndSet(true, false)) {
+ removed.countDown();
+ try {
+ assertTrue(resume.await(10, TimeUnit.SECONDS));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError(e);
+ }
+ }
+ return metric;
+ }
+ };
+ Field field = AbstractMetricManager.class.getDeclaredField("metrics");
+ field.setAccessible(true);
+ field.set(manager, registry);
+ AtomicReference<Thread> closingThread = new AtomicReference<>();
+ AtomicReference<Thread> restartingThread = new AtomicReference<>();
+ CountDownLatch restarting = new CountDownLatch(1);
+ Future<?> close =
+ executor.submit(
+ () -> {
+ closingThread.set(Thread.currentThread());
+ closing.close();
+ });
+ assertTrue(removed.await(5, TimeUnit.SECONDS));
+ Future<?> restart =
+ executor.submit(
+ () -> {
+ restartingThread.set(Thread.currentThread());
+ restarting.countDown();
+ realService.restartService();
+ });
+ assertTrue(restarting.await(5, TimeUnit.SECONDS));
+ await()
+ .atMost(5, TimeUnit.SECONDS)
+ .until(
+ () -> {
+ ThreadInfo info =
+ ManagementFactory.getThreadMXBean()
+ .getThreadInfo(restartingThread.get().getId());
+ return restart.isDone()
+ || (info != null
+ && info.getThreadState() == Thread.State.BLOCKED
+ && info.getLockOwnerId() ==
closingThread.get().getId());
+ });
+ assertFalse(restart.isDone());
+ // The core registry must not change while removal is paused, even
before metric-set rebind.
+ assertSame(registry, manager.getAllMetrics());
+ assertEquals(1, executor.submit(liveGauge::getValue).get(5,
TimeUnit.SECONDS), 0);
+ resume.countDown();
+ close.get(5, TimeUnit.SECONDS);
+ restart.get(5, TimeUnit.SECONDS);
+ assertEquals(8, realService.getAllMetrics().size());
+ assertMetrics(realService.getAllMetrics(), "live", live.getPool());
+ assertJmxMetrics(ManagementFactory.getPlatformMBeanServer(),
realService.getAllMetrics());
+ } finally {
+ resume.countDown();
+ executor.shutdownNow();
+ assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
+ realService.removeMetricSet(metrics);
+ realService.stopService();
+ }
+ }
+
+ @Test
+ public void testMetricCleanupFailureDoesNotFailClose() {
+ ClientManager<String, Object> manager = createManager("pool");
+ service.beforeRemove =
+ () -> {
+ throw new IllegalStateException("metric cleanup failure");
+ };
+ try {
+ manager.close();
+ assertTrue(manager.getPool().isClosed());
+ } finally {
+ service.beforeRemove = () -> {};
+ }
+ }
+
+ @Test
+ public void testMetricCleanupFailureDoesNotMaskPoolCloseFailure() {
+ @SuppressWarnings("unchecked")
+ GenericKeyedObjectPool<String, Object> pool =
mock(GenericKeyedObjectPool.class);
+ ClientManager<String, Object> manager =
+ new ClientManager<>(
+ owner -> {
+ metrics.registerClientManager("pool", pool);
+ return pool;
+ });
+ managers.add(manager);
+ IllegalStateException poolFailure = new IllegalStateException("pool close
failure");
+ doThrow(poolFailure).when(pool).close();
+ service.beforeRemove =
+ () -> {
+ throw new IllegalStateException("metric cleanup failure");
+ };
+ try {
+ assertSame(poolFailure, assertThrows(IllegalStateException.class,
manager::close));
+ } finally {
+ doNothing().when(pool).close();
+ service.beforeRemove = () -> {};
+ }
+ }
+
+ @Test
+ public void testRegistrationAndSamplingDuringUnbind() throws Exception {
+ ClientManager<String, Object> first = createManager("first");
+ first.borrowClient("node");
+ createManager("second");
+ AutoGauge inFlight =
+ service.getAutoGauge(
+ Metric.CLIENT_MANAGER.toString(),
+ MetricLevel.IMPORTANT,
+ Tag.NAME.toString(),
+ "client_manager_num_active",
+ Tag.TYPE.toString(),
+ "first");
+ CountDownLatch removing = new CountDownLatch(1);
+ CountDownLatch resume = new CountDownLatch(1);
+ AtomicBoolean pauseOnce = new AtomicBoolean(true);
+ service.beforeRemove =
+ () -> {
+ if (pauseOnce.compareAndSet(true, false)) {
+ removing.countDown();
+ try {
+ assertTrue(resume.await(10, TimeUnit.SECONDS));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError(e);
+ }
+ }
+ };
+ ExecutorService executor = Executors.newFixedThreadPool(3);
+ try {
+ Future<?> stop = executor.submit(service::stopService);
+ assertTrue(removing.await(5, TimeUnit.SECONDS));
+ AtomicReference<Thread> registrationThread = new AtomicReference<>();
+ CountDownLatch registering = new CountDownLatch(1);
+ Future<ClientManager<String, Object>> registration =
+ executor.submit(
+ () -> {
+ registrationThread.set(Thread.currentThread());
+ registering.countDown();
+ return createManager("third");
+ });
+ assertTrue(registering.await(5, TimeUnit.SECONDS));
+ await()
+ .atMost(5, TimeUnit.SECONDS)
+ .until(
+ () ->
+ registration.isDone()
+ || registrationThread.get().getState() ==
Thread.State.BLOCKED);
+ assertFalse(registration.isDone());
+ // Sampling must not acquire the lifecycle lock held by the paused
unbind.
+ assertEquals(1, executor.submit(inFlight::getValue).get(5,
TimeUnit.SECONDS), 0);
+ resume.countDown();
+ stop.get(5, TimeUnit.SECONDS);
+ registration.get(5, TimeUnit.SECONDS);
+ assertTrue(service.getAllMetrics().isEmpty());
+
+ service.startService();
+ assertEquals(24, service.getAllMetrics().size());
+ assertMetrics(service.getAllMetrics(), "first", first.getPool());
+ } finally {
+ resume.countDown();
+ executor.shutdownNow();
+ assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
+ }
+ }
+
+ private ClientManager<String, Object> createManager(String name) {
+ ClientManager<String, Object> manager =
+ new ClientManager<>(
+ owner -> {
+ GenericKeyedObjectPoolConfig<Object> poolConfig =
+ new GenericKeyedObjectPoolConfig<>();
+ poolConfig.setJmxEnabled(false);
+ GenericKeyedObjectPool<String, Object> pool =
+ new GenericKeyedObjectPool<>(
+ new BaseKeyedPooledObjectFactory<String, Object>() {
+ @Override
+ public Object create(String key) {
+ return new Object();
+ }
+
+ @Override
+ public PooledObject<Object> wrap(Object value) {
+ return new DefaultPooledObject<>(value);
+ }
+ },
+ poolConfig);
+ metrics.registerClientManager(name, pool);
+ return pool;
+ });
+ managers.add(manager);
+ return manager;
+ }
+
+ private void assertMetrics(
+ Map<MetricInfo, IMetric> actual, String poolName,
GenericKeyedObjectPool<?, ?> pool) {
+ Map<String, Double> expected = new HashMap<>();
+ expected.put("client_manager_num_active", (double) pool.getNumActive());
+ expected.put("client_manager_num_idle", (double) pool.getNumIdle());
+ expected.put("client_manager_borrowed_count", (double)
pool.getBorrowedCount());
+ expected.put("client_manager_created_count", (double)
pool.getCreatedCount());
+ expected.put("client_manager_destroyed_count", (double)
pool.getDestroyedCount());
+ expected.put("client_manager_mean_active_time", (double)
pool.getMeanActiveTimeMillis());
+ expected.put(
+ "client_manager_mean_borrow_wait_time", (double)
pool.getMeanBorrowWaitTimeMillis());
+ expected.put("client_manager_mean_idle_time", (double)
pool.getMeanIdleTimeMillis());
+ expected.forEach(
+ (name, value) -> {
+ MetricInfo key =
+ new MetricInfo(
+ MetricType.AUTO_GAUGE,
+ Metric.CLIENT_MANAGER.toString(),
+ Tag.NAME.toString(),
+ name,
+ Tag.TYPE.toString(),
+ poolName);
+ assertTrue(actual.get(key) instanceof AutoGauge);
+ assertEquals(value, ((AutoGauge) actual.get(key)).getValue(), 0);
+ });
+ }
+
+ private static class TestMetricService extends DoNothingMetricService {
+ private Runnable beforeRemove = () -> {};
+
+ @Override
+ protected void loadManager() {
+ metricManager = IoTDBMetricManager.getInstance();
+ }
+
+ @Override
+ public void remove(MetricType type, String metric, String... tags) {
+ beforeRemove.run();
+ super.remove(type, metric, tags);
+ }
+ }
+}
diff --git
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/service/metric/MetricReporterSwitchTest.java
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/service/metric/MetricReporterSwitchTest.java
new file mode 100644
index 00000000000..b2da6ca9708
--- /dev/null
+++
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/service/metric/MetricReporterSwitchTest.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.iotdb.commons.service.metric;
+
+import org.apache.iotdb.metrics.AbstractMetricManager;
+import org.apache.iotdb.metrics.config.MetricConfig;
+import org.apache.iotdb.metrics.config.MetricConfigDescriptor;
+import org.apache.iotdb.metrics.config.ReloadLevel;
+import org.apache.iotdb.metrics.core.reporter.IoTDBJmxReporter;
+import org.apache.iotdb.metrics.reporter.JmxReporter;
+import org.apache.iotdb.metrics.type.AutoGauge;
+import org.apache.iotdb.metrics.type.IMetric;
+import org.apache.iotdb.metrics.utils.MetricInfo;
+import org.apache.iotdb.metrics.utils.MetricLevel;
+import org.apache.iotdb.metrics.utils.ReporterType;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import java.lang.management.ManagementFactory;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class MetricReporterSwitchTest {
+ private static final String METRIC = "reporter_switch_value";
+ private final MetricConfig config =
MetricConfigDescriptor.getInstance().getMetricConfig();
+ private final MetricService service = MetricService.getInstance();
+ private final AtomicInteger oldValue = new AtomicInteger(1);
+ private final AtomicInteger newValue = new AtomicInteger(2);
+ private MetricLevel previousLevel;
+ private String previousReporters;
+
+ @Before
+ public void setUp() {
+ previousLevel = config.getMetricLevel();
+ previousReporters =
+
config.getMetricReporterList().stream().map(Enum::name).collect(Collectors.joining(","));
+ config.setMetricLevel(MetricLevel.IMPORTANT);
+ config.setMetricReporterList("JMX");
+ service.startService();
+ }
+
+ @After
+ public void tearDown() {
+ service.getMetricManager().setBindJmxReporter(null);
+ service.stopService();
+ config.setMetricLevel(previousLevel);
+ config.setMetricReporterList(previousReporters);
+ }
+
+ @Test
+ public void testDisablingReporterDuringGaugeReplacement() throws Exception {
+ replaceGaugeWhileReloading("");
+ }
+
+ @Test
+ public void testReloadingReporterKeepsCapturedNotificationTarget() throws
Exception {
+ replaceGaugeWhileReloading("JMX");
+ }
+
+ private void replaceGaugeWhileReloading(String reporters) throws Exception {
+ AbstractMetricManager manager = service.getMetricManager();
+ IoTDBJmxReporter delegate = IoTDBJmxReporter.getInstance();
+ CountDownLatch removalCallback = new CountDownLatch(1);
+ CountDownLatch resume = new CountDownLatch(1);
+ AtomicInteger registrations = new AtomicInteger();
+ AtomicInteger removals = new AtomicInteger();
+ // Preserve real JMX behavior while pausing between replacement's
remove/add notifications.
+ manager.setBindJmxReporter(
+ new JmxReporter() {
+ @Override
+ public void registerMetric(IMetric metric, MetricInfo info) {
+ registrations.incrementAndGet();
+ delegate.registerMetric(metric, info);
+ }
+
+ @Override
+ public void unregisterMetric(IMetric metric, MetricInfo info) {
+ removals.incrementAndGet();
+ delegate.unregisterMetric(metric, info);
+ removalCallback.countDown();
+ try {
+ assertTrue(resume.await(10, TimeUnit.SECONDS));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ public boolean start() {
+ return delegate.start();
+ }
+
+ @Override
+ public boolean stop() {
+ return delegate.stop();
+ }
+
+ @Override
+ public ReporterType getReporterType() {
+ return ReporterType.JMX;
+ }
+ });
+ manager.createAutoGauge(METRIC, MetricLevel.IMPORTANT, oldValue,
AtomicInteger::get);
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ Future<AutoGauge> replacement =
+ executor.submit(
+ () ->
+ manager.createAutoGauge(
+ METRIC, MetricLevel.IMPORTANT, newValue,
AtomicInteger::get));
+ assertTrue(removalCallback.await(5, TimeUnit.SECONDS));
+ config.setMetricReporterList(reporters);
+ service.reloadService(ReloadLevel.RESTART_REPORTER);
+ resume.countDown();
+ assertEquals(2, replacement.get(5, TimeUnit.SECONDS).getValue(), 0);
+ // Reload either clears the binding or replaces our wrapper with the
real JMX reporter.
+ // Both notifications for the in-flight replacement must still use the
captured wrapper.
+ assertEquals(2, registrations.get());
+ assertEquals(1, removals.get());
+ MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+ ObjectName name =
+ new ObjectName("org.apache.iotdb.metrics:name=" + METRIC +
",type=IoTDBAutoGauge");
+ if (reporters.isEmpty()) {
+ assertFalse(server.isRegistered(name));
+ } else {
+ assertEquals(2, (double) server.getAttribute(name, "Value"), 0);
+ }
+ } finally {
+ resume.countDown();
+ executor.shutdownNow();
+ assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
+ }
+ }
+}