Add BlockHound integration that detects blocking calls in event loops (#9687)

Motivation:

Netty is an asynchronous framework.
If somebody uses a blocking call inside Netty's event loops,
it may lead to a severe performance degradation.
BlockHound is a tool that helps detecting such calls.

Modifications:

This change adds a BlockHound's SPI integration that marks
threads created by Netty (`FastThreadLocalThread`s) as non-blocking.
It also marks some of Netty's internal methods as whitelisted
as they are required to run the event loops.

Result:

When BlockHound is installed, any blocking call inside event loops
is intercepted and reported (by default an error will be thrown).
This commit is contained in:
Sergei Egorov 2019-10-25 16:14:14 +03:00 committed by Norman Maurer
parent e4d400fa4a
commit 2854c2c473
6 changed files with 222 additions and 0 deletions

View File

@ -78,6 +78,11 @@
<artifactId>log4j-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.tools</groupId>
<artifactId>blockhound</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>

View File

@ -0,0 +1,87 @@
/*
* Copyright 2019 The Netty Project
*
* The Netty Project 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 io.netty.util.internal;
import io.netty.util.concurrent.FastThreadLocalThread;
import reactor.blockhound.BlockHound;
import reactor.blockhound.integration.BlockHoundIntegration;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Contains classes that must be have public visibility but are not public API.
*/
class Hidden {
/**
* This class integrates Netty with BlockHound.
* <p>
* It is public but only because of the ServiceLoader's limitations
* and SHOULD NOT be considered a public API.
*/
@UnstableApi
@SuppressJava6Requirement(reason = "BlockHound is Java 8+, but this class is only loaded by it's SPI")
public static final class NettyBlockHoundIntegration implements BlockHoundIntegration {
@Override
public void applyTo(BlockHound.Builder builder) {
builder.allowBlockingCallsInside(
"io.netty.channel.nio.NioEventLoop",
"handleLoopException"
);
builder.allowBlockingCallsInside(
"io.netty.channel.kqueue.KQueueEventLoop",
"handleLoopException"
);
builder.allowBlockingCallsInside(
"io.netty.channel.epoll.EpollEventLoop",
"handleLoopException"
);
builder.allowBlockingCallsInside(
"io.netty.util.HashedWheelTimer$Worker",
"waitForNextTick"
);
builder.allowBlockingCallsInside(
"io.netty.util.concurrent.SingleThreadEventExecutor",
"confirmShutdown"
);
builder.nonBlockingThreadPredicate(new Function<Predicate<Thread>, Predicate<Thread>>() {
@Override
public Predicate<Thread> apply(final Predicate<Thread> p) {
return new Predicate<Thread>() {
@Override
@SuppressJava6Requirement(reason = "Predicate#test")
public boolean test(Thread thread) {
return p.test(thread) || thread instanceof FastThreadLocalThread;
}
};
}
});
}
@Override
public int compareTo(BlockHoundIntegration o) {
return 0;
}
}
}

View File

@ -0,0 +1,14 @@
# Copyright 2019 The Netty Project
#
# The Netty Project 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.
io.netty.util.internal.Hidden$NettyBlockHoundIntegration

View File

@ -368,6 +368,7 @@
<module>testsuite-osgi</module>
<module>testsuite-shading</module>
<module>testsuite-native-image</module>
<module>transport-blockhound-tests</module>
<module>microbench</module>
<module>bom</module>
</modules>
@ -655,6 +656,13 @@
<version>${log4j2.version}</version>
<scope>test</scope>
</dependency>
<!-- BlockHound integration -->
<dependency>
<groupId>io.projectreactor.tools</groupId>
<artifactId>blockhound</artifactId>
<version>1.0.1.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2019 The Netty Project
~
~ The Netty Project 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.netty</groupId>
<artifactId>netty-parent</artifactId>
<version>4.1.43.Final-SNAPSHOT</version>
</parent>
<artifactId>netty-transport-blockhound-tests</artifactId>
<packaging>jar</packaging>
<description>
Tests for the BlockHound integration.
</description>
<name>Netty/Transport/BlockHound/Tests</name>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<skipJapicmp>true</skipJapicmp>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>netty-transport</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.projectreactor.tools</groupId>
<artifactId>blockhound</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,55 @@
/*
* Copyright 2019 The Netty Project
* The Netty Project 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 io.netty.util.internal;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.junit.BeforeClass;
import org.junit.Test;
import reactor.blockhound.BlockHound;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class NettyBlockHoundIntegrationTest {
@BeforeClass
public static void setUpClass() {
BlockHound.install();
}
@Test
public void testBlockingCallsInNettyThreads() throws Exception {
final FutureTask<Void> future = new FutureTask<>(() -> {
Thread.sleep(0);
return null;
});
GlobalEventExecutor.INSTANCE.execute(future);
try {
future.get(5, TimeUnit.SECONDS);
fail("Expected an exception due to a blocking call but none was thrown");
} catch (ExecutionException e) {
Throwable throwable = e.getCause();
assertNotNull("An exception was thrown", throwable);
assertTrue("Blocking call was reported", throwable.getMessage().contains("Blocking call"));
}
}
}