mirror of https://github.com/longtai-cn/hippo4j
feat(MaximumActiveThreadCountChecker): add new plugin that checks whether the maximum number of threads in the thread pool is exceeded(#1208)
parent
46334f8927
commit
425f27aef2
@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* 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 cn.hippo4j.core.plugin.impl;
|
||||||
|
|
||||||
|
import cn.hippo4j.core.plugin.ExecuteAwarePlugin;
|
||||||
|
import cn.hippo4j.core.plugin.PluginRuntime;
|
||||||
|
import cn.hippo4j.core.plugin.manager.ThreadPoolPluginSupport;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.concurrent.RunnableFuture;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>A plugin that checks whether the maximum number of threads in the thread pool is exceeded.
|
||||||
|
*
|
||||||
|
* <p>When task is submitted to the thread pool, before the current worker thread executes the task:
|
||||||
|
* <ol>
|
||||||
|
* <li>check whether the maximum number of threads in the thread pool is exceeded;</li>
|
||||||
|
* <li>
|
||||||
|
* if already exceeded, interrupt the worker thread,
|
||||||
|
* and throw an {@link IllegalMaximumActiveCountException} exception to destroy the worker thread;
|
||||||
|
* </li>
|
||||||
|
* <li>if {@link #enableSubmitTaskAfterCheckFail} is true, re submit the task to the thread pool after check fail;</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p><b>NOTE</b>: if custom {@link Thread.UncaughtExceptionHandler} is set for the thread pool,
|
||||||
|
* it may catch the {@link IllegalMaximumActiveCountException} exception and cause the worker thread to not be destroyed.
|
||||||
|
*
|
||||||
|
* @author huangchengxing
|
||||||
|
* @see IllegalMaximumActiveCountException
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MaximumActiveThreadCountChecker implements ExecuteAwarePlugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thread pool.
|
||||||
|
*/
|
||||||
|
public final ThreadPoolPluginSupport threadPoolPluginSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to re-deliver the task to the thread pool after the maximum number of threads is exceeded.
|
||||||
|
*/
|
||||||
|
private final AtomicBoolean enableSubmitTaskAfterCheckFail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create {@link MaximumActiveThreadCountChecker}.
|
||||||
|
*
|
||||||
|
* @param threadPoolPluginSupport thread pool
|
||||||
|
* @param enableSubmitTaskAfterCheckFail whether to re-deliver the task to the thread pool after the maximum number of threads is exceeded.
|
||||||
|
*/
|
||||||
|
public MaximumActiveThreadCountChecker(ThreadPoolPluginSupport threadPoolPluginSupport, boolean enableSubmitTaskAfterCheckFail) {
|
||||||
|
this.threadPoolPluginSupport = threadPoolPluginSupport;
|
||||||
|
this.enableSubmitTaskAfterCheckFail = new AtomicBoolean(enableSubmitTaskAfterCheckFail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether to re-deliver the task to the thread pool after the maximum number of threads is exceeded.
|
||||||
|
*
|
||||||
|
* @param enableSubmitTaskAfterCheckFail whether to re-deliver the task to the thread pool after the maximum number of threads is exceeded.
|
||||||
|
*/
|
||||||
|
public void setEnableSubmitTaskAfterCheckFail(boolean enableSubmitTaskAfterCheckFail) {
|
||||||
|
this.enableSubmitTaskAfterCheckFail.set(enableSubmitTaskAfterCheckFail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether to re-deliver the task to the thread pool after the maximum number of threads is exceeded.
|
||||||
|
*
|
||||||
|
* @return whether to re-deliver the task to the thread pool after the maximum number of threads is exceeded.
|
||||||
|
*/
|
||||||
|
public boolean isEnableSubmitTaskAfterCheckFail() {
|
||||||
|
return enableSubmitTaskAfterCheckFail.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin runtime info.
|
||||||
|
*
|
||||||
|
* @return plugin runtime info
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public PluginRuntime getPluginRuntime() {
|
||||||
|
return new PluginRuntime(getId())
|
||||||
|
.addInfo("enableSubmitTaskAfterCheckFail", enableSubmitTaskAfterCheckFail.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Check the maximum number of threads in the thread pool before the task is executed,
|
||||||
|
* if the maximum number of threads is exceeded,
|
||||||
|
* an {@link IllegalMaximumActiveCountException} will be thrown.
|
||||||
|
*
|
||||||
|
* @param thread thread of executing task
|
||||||
|
* @param runnable task
|
||||||
|
* @throws IllegalMaximumActiveCountException thread if the maximum number of threads is exceeded
|
||||||
|
* @see ThreadPoolExecutor#beforeExecute
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void beforeExecute(Thread thread, Runnable runnable) {
|
||||||
|
ThreadPoolExecutor threadPoolExecutor = threadPoolPluginSupport.getThreadPoolExecutor();
|
||||||
|
int activeCount = threadPoolExecutor.getActiveCount();
|
||||||
|
int maximumPoolSize = threadPoolExecutor.getMaximumPoolSize();
|
||||||
|
if (activeCount > maximumPoolSize) {
|
||||||
|
// redelivery task if necessary
|
||||||
|
if (enableSubmitTaskAfterCheckFail.get()) {
|
||||||
|
log.warn(
|
||||||
|
"The maximum number of threads in the thread pool '{}' has been exceeded(activeCount={}, maximumPoolSize={}), task '{}' will redelivery",
|
||||||
|
threadPoolPluginSupport.getThreadPoolId(), activeCount, maximumPoolSize, runnable);
|
||||||
|
submitTaskAfterCheckFail(runnable);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
"The maximum number of threads in the thread pool '{}' has been exceeded(activeCount={}, maximumPoolSize={}), task '{}' will be discarded.",
|
||||||
|
threadPoolPluginSupport.getThreadPoolId(), activeCount, maximumPoolSize, runnable);
|
||||||
|
}
|
||||||
|
interruptAndThrowException(thread, activeCount, maximumPoolSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Submit task to thread pool after check fail.
|
||||||
|
*
|
||||||
|
* @param runnable task
|
||||||
|
*/
|
||||||
|
protected void submitTaskAfterCheckFail(Runnable runnable) {
|
||||||
|
if (runnable instanceof RunnableFuture) {
|
||||||
|
RunnableFuture<?> future = (RunnableFuture<?>) runnable;
|
||||||
|
if (future.isDone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (future.isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
threadPoolPluginSupport.getThreadPoolExecutor().execute(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void interruptAndThrowException(Thread thread, int activeCount, int maximumPoolSize) {
|
||||||
|
thread.interrupt();
|
||||||
|
throw new IllegalMaximumActiveCountException(
|
||||||
|
"The maximum number of threads in the thread pool '" + threadPoolPluginSupport.getThreadPoolId()
|
||||||
|
+ "' has been exceeded(activeCount=" + activeCount + ", maximumPoolSize=" + maximumPoolSize + ").");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link RuntimeException} that indicates that the maximum number of threads in the thread pool has been exceeded.
|
||||||
|
*/
|
||||||
|
protected static class IllegalMaximumActiveCountException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new runtime exception with the specified detail message.
|
||||||
|
* The cause is not initialized, and may subsequently be initialized by a
|
||||||
|
* call to {@link #initCause}.
|
||||||
|
*
|
||||||
|
* @param message the detail message. The detail message is saved for
|
||||||
|
* later retrieval by the {@link #getMessage()} method.
|
||||||
|
*/
|
||||||
|
public IllegalMaximumActiveCountException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* 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 cn.hippo4j.core.plugin.impl;
|
||||||
|
|
||||||
|
import cn.hippo4j.core.executor.ExtensibleThreadPoolExecutor;
|
||||||
|
import cn.hippo4j.core.plugin.ExecuteAwarePlugin;
|
||||||
|
import cn.hippo4j.core.plugin.PluginRuntime;
|
||||||
|
import cn.hippo4j.core.plugin.manager.DefaultThreadPoolPluginManager;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* test for {@link MaximumActiveThreadCountChecker}
|
||||||
|
*
|
||||||
|
* @author huangchengxing
|
||||||
|
*/
|
||||||
|
public class MaximumActiveThreadCountCheckerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetPluginRuntime() {
|
||||||
|
ExtensibleThreadPoolExecutor executor = new ExtensibleThreadPoolExecutor(
|
||||||
|
"test", new DefaultThreadPoolPluginManager(),
|
||||||
|
5, 5, 0L, TimeUnit.MILLISECONDS,
|
||||||
|
new ArrayBlockingQueue<>(10), t -> new Thread(t, UUID.randomUUID().toString()), new ThreadPoolExecutor.AbortPolicy());
|
||||||
|
MaximumActiveThreadCountChecker checker = new MaximumActiveThreadCountChecker(executor, true);
|
||||||
|
Assert.assertEquals(checker.getClass().getSimpleName(), checker.getPluginRuntime().getPluginId());
|
||||||
|
// check plugin info
|
||||||
|
List<PluginRuntime.Info> infoList = checker.getPluginRuntime().getInfoList();
|
||||||
|
Assert.assertEquals(1, infoList.size());
|
||||||
|
Assert.assertEquals("enableSubmitTaskAfterCheckFail", infoList.get(0).getName());
|
||||||
|
Assert.assertEquals(true, infoList.get(0).getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWhenEnableSubmitTaskAfterCheckFail() {
|
||||||
|
int maximumThreadNum = 3;
|
||||||
|
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);
|
||||||
|
ExtensibleThreadPoolExecutor executor = new ExtensibleThreadPoolExecutor(
|
||||||
|
"test", new DefaultThreadPoolPluginManager(),
|
||||||
|
maximumThreadNum, maximumThreadNum, 0L, TimeUnit.MILLISECONDS,
|
||||||
|
queue, t -> new Thread(t, UUID.randomUUID().toString()), new ThreadPoolExecutor.AbortPolicy());
|
||||||
|
MaximumActiveThreadCountChecker checker = new MaximumActiveThreadCountChecker(executor, true);
|
||||||
|
Assert.assertTrue(checker.isEnableSubmitTaskAfterCheckFail());
|
||||||
|
executor.register(checker);
|
||||||
|
WaitBeforeExecute waitBeforeExecute = new WaitBeforeExecute(0L);
|
||||||
|
executor.register(waitBeforeExecute);
|
||||||
|
|
||||||
|
// create 2 workers and block them
|
||||||
|
CountDownLatch latch1 = submitTaskForBlockingThread(maximumThreadNum - 1, executor);
|
||||||
|
try {
|
||||||
|
// wait for the 2 workers to be executed task
|
||||||
|
Thread.sleep(500L);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* after 2 worker blocked, submit task and create the last worker, then change the maximum number of pool before the last task actually executed by worker, make plugin throw exception and
|
||||||
|
* re-deliver the task to the queue
|
||||||
|
*/
|
||||||
|
waitBeforeExecute.setWaitBeforeExecute(200L);
|
||||||
|
CountDownLatch latch2 = submitTaskForBlockingThread(1, executor);
|
||||||
|
executor.setCorePoolSize(maximumThreadNum - 1);
|
||||||
|
executor.setMaximumPoolSize(maximumThreadNum - 1);
|
||||||
|
|
||||||
|
// wait for plugin to re-deliver the task to the queue
|
||||||
|
try {
|
||||||
|
Thread.sleep(500L);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
Assert.assertEquals(1, queue.size());
|
||||||
|
// last worker destroyed due to the exception which thrown by plugin
|
||||||
|
Assert.assertEquals(2, executor.getActiveCount());
|
||||||
|
|
||||||
|
// free resources
|
||||||
|
latch1.countDown();
|
||||||
|
latch2.countDown();
|
||||||
|
executor.shutdown();
|
||||||
|
while (executor.isTerminated()) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWhenNotEnableSubmitTaskAfterCheckFail() {
|
||||||
|
int maximumThreadNum = 3;
|
||||||
|
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);
|
||||||
|
ExtensibleThreadPoolExecutor executor = new ExtensibleThreadPoolExecutor(
|
||||||
|
"test", new DefaultThreadPoolPluginManager(),
|
||||||
|
maximumThreadNum, maximumThreadNum, 0L, TimeUnit.MILLISECONDS,
|
||||||
|
queue, t -> new Thread(t, UUID.randomUUID().toString()), new ThreadPoolExecutor.AbortPolicy());
|
||||||
|
MaximumActiveThreadCountChecker checker = new MaximumActiveThreadCountChecker(executor, true);
|
||||||
|
checker.setEnableSubmitTaskAfterCheckFail(false);
|
||||||
|
Assert.assertFalse(checker.isEnableSubmitTaskAfterCheckFail());
|
||||||
|
executor.register(checker);
|
||||||
|
WaitBeforeExecute waitBeforeExecute = new WaitBeforeExecute(0L);
|
||||||
|
executor.register(waitBeforeExecute);
|
||||||
|
|
||||||
|
// create 2 workers and block them
|
||||||
|
CountDownLatch latch1 = submitTaskForBlockingThread(maximumThreadNum - 1, executor);
|
||||||
|
try {
|
||||||
|
// wait for the 2 workers to be executed task
|
||||||
|
Thread.sleep(500L);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* after 2 worker blocked, submit task and create the last worker, then change the maximum number of pool before the last task actually executed by worker, make plugin throw exception and
|
||||||
|
* re-deliver the task to the queue
|
||||||
|
*/
|
||||||
|
waitBeforeExecute.setWaitBeforeExecute(200L);
|
||||||
|
CountDownLatch latch2 = submitTaskForBlockingThread(1, executor);
|
||||||
|
executor.setCorePoolSize(maximumThreadNum - 1);
|
||||||
|
executor.setMaximumPoolSize(maximumThreadNum - 1);
|
||||||
|
|
||||||
|
// wait for plugin abort the task
|
||||||
|
try {
|
||||||
|
Thread.sleep(500L);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
Assert.assertEquals(0, queue.size());
|
||||||
|
// last worker destroyed due to the exception which thrown by plugin
|
||||||
|
Assert.assertEquals(2, executor.getActiveCount());
|
||||||
|
|
||||||
|
// free resources
|
||||||
|
latch1.countDown();
|
||||||
|
latch2.countDown();
|
||||||
|
executor.shutdown();
|
||||||
|
while (executor.isTerminated()) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CountDownLatch submitTaskForBlockingThread(int num, ThreadPoolExecutor executor) {
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
for (int i = 0; i < num; i++) {
|
||||||
|
Runnable runnable = () -> {
|
||||||
|
System.out.println(Thread.currentThread().getName() + " start");
|
||||||
|
try {
|
||||||
|
latch.await();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
System.out.println(Thread.currentThread().getName() + " has been interrupted");
|
||||||
|
}
|
||||||
|
System.out.println(Thread.currentThread().getName() + " completed");
|
||||||
|
};
|
||||||
|
System.out.println("submit task@" + runnable.hashCode());
|
||||||
|
executor.execute(runnable);
|
||||||
|
}
|
||||||
|
return latch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@AllArgsConstructor
|
||||||
|
private static class WaitBeforeExecute implements ExecuteAwarePlugin {
|
||||||
|
|
||||||
|
private long waitBeforeExecute;
|
||||||
|
@Override
|
||||||
|
public void beforeExecute(Thread thread, Runnable runnable) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(waitBeforeExecute);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue