Skip to content

Commit 5a437f5

Browse files
authored
Fix concurrent JVM thread state metric refresh (#18950)
1 parent 9ffa893 commit 5a437f5

2 files changed

Lines changed: 145 additions & 1 deletion

File tree

‎iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/metricsets/jvm/JvmThreadMetrics.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ public void unbindFrom(AbstractMetricService metricService) {
104104
}
105105

106106
// VisibleForTesting
107-
static long getThreadStateCount(ThreadMXBean threadBean, Thread.State state) {
107+
static synchronized long getThreadStateCount(ThreadMXBean threadBean, Thread.State state) {
108+
// Keep reads and refreshes under the same lock to avoid observing a partially rebuilt EnumMap.
108109
checkAndUpdate(threadBean);
109110
return threadStateCountMap.getOrDefault(state, 0);
110111
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.iotdb.metrics.metricsets.jvm;
21+
22+
import org.junit.After;
23+
import org.junit.Before;
24+
import org.junit.Test;
25+
26+
import java.lang.management.ManagementFactory;
27+
import java.lang.management.ThreadInfo;
28+
import java.lang.management.ThreadMXBean;
29+
import java.lang.reflect.Field;
30+
import java.lang.reflect.Proxy;
31+
import java.util.concurrent.CountDownLatch;
32+
import java.util.concurrent.ExecutorService;
33+
import java.util.concurrent.Executors;
34+
import java.util.concurrent.Future;
35+
import java.util.concurrent.TimeUnit;
36+
import java.util.concurrent.atomic.AtomicInteger;
37+
import java.util.concurrent.atomic.AtomicReference;
38+
import java.util.concurrent.locks.LockSupport;
39+
import java.util.function.Supplier;
40+
41+
import static org.junit.Assert.assertEquals;
42+
import static org.junit.Assert.assertTrue;
43+
44+
public class JvmThreadMetricsTest {
45+
46+
@Before
47+
@After
48+
public void expireCache() throws Exception {
49+
Field lastUpdateTime = JvmThreadMetrics.class.getDeclaredField("lastUpdateTime");
50+
lastUpdateTime.setAccessible(true);
51+
synchronized (JvmThreadMetrics.class) {
52+
lastUpdateTime.setLong(null, 0L);
53+
}
54+
}
55+
56+
// Count two live threads, ignore a terminated thread's null info, and return zero for an absent
57+
// state without resampling within the cache interval.
58+
@Test
59+
public void testThreadStateCountsAndMissingState() {
60+
ThreadInfo info =
61+
ManagementFactory.getThreadMXBean().getThreadInfo(Thread.currentThread().getId());
62+
AtomicInteger samples = new AtomicInteger();
63+
ThreadMXBean threadBean =
64+
threadBean(
65+
() -> {
66+
samples.incrementAndGet();
67+
return new ThreadInfo[] {info, info, null};
68+
});
69+
70+
assertEquals(2L, JvmThreadMetrics.getThreadStateCount(threadBean, info.getThreadState()));
71+
assertEquals(0L, JvmThreadMetrics.getThreadStateCount(threadBean, Thread.State.TERMINATED));
72+
assertEquals(1, samples.get());
73+
}
74+
75+
// Pause a refresh after clearing the cache, then read the same state concurrently. Both calls
76+
// must return the complete sample rather than a temporarily absent entry.
77+
@Test
78+
public void testConcurrentReadDuringRefresh() throws Exception {
79+
ThreadInfo info =
80+
ManagementFactory.getThreadMXBean().getThreadInfo(Thread.currentThread().getId());
81+
CountDownLatch refreshStarted = new CountDownLatch(1);
82+
CountDownLatch resumeRefresh = new CountDownLatch(1);
83+
CountDownLatch readStarted = new CountDownLatch(1);
84+
AtomicReference<Thread> readerThread = new AtomicReference<>();
85+
ThreadMXBean threadBean =
86+
threadBean(
87+
() -> {
88+
refreshStarted.countDown();
89+
try {
90+
assertTrue(resumeRefresh.await(10, TimeUnit.SECONDS));
91+
} catch (InterruptedException e) {
92+
Thread.currentThread().interrupt();
93+
throw new AssertionError(e);
94+
}
95+
return new ThreadInfo[] {info};
96+
});
97+
ExecutorService executor = Executors.newFixedThreadPool(2);
98+
try {
99+
Future<Long> refresh =
100+
executor.submit(
101+
() -> JvmThreadMetrics.getThreadStateCount(threadBean, info.getThreadState()));
102+
assertTrue(refreshStarted.await(5, TimeUnit.SECONDS));
103+
Future<Long> read =
104+
executor.submit(
105+
() -> {
106+
readerThread.set(Thread.currentThread());
107+
readStarted.countDown();
108+
return JvmThreadMetrics.getThreadStateCount(threadBean, info.getThreadState());
109+
});
110+
assertTrue(readStarted.await(5, TimeUnit.SECONDS));
111+
112+
// Ensure the reader reaches the cache lookup before allowing the refresh to finish.
113+
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
114+
while (!read.isDone()
115+
&& readerThread.get().getState() != Thread.State.BLOCKED
116+
&& System.nanoTime() < deadline) {
117+
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
118+
}
119+
assertTrue(read.isDone() || readerThread.get().getState() == Thread.State.BLOCKED);
120+
resumeRefresh.countDown();
121+
122+
assertEquals(1L, refresh.get(5, TimeUnit.SECONDS).longValue());
123+
assertEquals(1L, read.get(5, TimeUnit.SECONDS).longValue());
124+
} finally {
125+
resumeRefresh.countDown();
126+
executor.shutdownNow();
127+
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
128+
}
129+
}
130+
131+
private static ThreadMXBean threadBean(Supplier<ThreadInfo[]> threadInfos) {
132+
return (ThreadMXBean)
133+
Proxy.newProxyInstance(
134+
JvmThreadMetricsTest.class.getClassLoader(),
135+
new Class<?>[] {ThreadMXBean.class},
136+
(proxy, method, args) ->
137+
switch (method.getName()) {
138+
case "getAllThreadIds" -> new long[] {1L, 2L, 3L};
139+
case "getThreadInfo" -> threadInfos.get();
140+
default -> throw new UnsupportedOperationException(method.getName());
141+
});
142+
}
143+
}

0 commit comments

Comments
 (0)