|
| 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