Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
245 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,3 +11,8 @@ build/ | |
.vscode/ | ||
|
||
cmake-build-*/ | ||
|
||
|
||
.project | ||
.classpath | ||
bin/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
## Table of contents | ||
* [Quick Start](quick_start.md) | ||
* [Customization](customization.md) | ||
* [How it works](how_it_works.md) | ||
* [Writing custom integrations](custom_integrations.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Custom integrations | ||
|
||
BlockHound can be extended without changing its code by using | ||
[the JVM's SPI mechanism](https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html). | ||
|
||
You will need to implement `reactor.blockhound.integration.BlockHoundIntegration` interface | ||
and add the implementor to `META-INF/services/reactor.blockhound.integration.BlockHoundIntegration` file. | ||
|
||
> ℹ️ **Hint:** consider using [Google's AutoService](https://github.com/google/auto/tree/master/service) for it: | ||
> ```java | ||
> @AutoService(BlockHoundIntegration.class) | ||
> public class MyIntegration implements BlockHoundIntegration { | ||
> // ... | ||
> } | ||
> ``` | ||
## Writing integrations | ||
An integration is just a consumer of BlockHound's `Builder` and uses the same API as described in [customization](customization.md). | ||
|
||
Here is an example: | ||
```java | ||
public class MyIntegration implements BlockHoundIntegration { | ||
|
||
@Override | ||
public void applyTo(BlockHound.Builder builder) { | ||
builder.nonBlockingThreadPredicate(current -> { | ||
return current.or(t -> { | ||
if (t.getName() == null) { | ||
return false; | ||
} | ||
return t.getName().contains("my-pool-"); | ||
}); | ||
}); | ||
} | ||
} | ||
``` | ||
|
||
|
||
BlockHound's built-in integrations use the same mechanism and can be used as more advanced examples: | ||
https://github.com/reactor/BlockHound/tree/master/agent/src/main/java/reactor/blockhound/integration |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# Customization | ||
|
||
BlockHound provides three means of usage: | ||
1. `BlockHound.install()` - will use `ServiceLoader` to load all known `reactor.blockhound.integration.BlockHoundIntegration`s | ||
1. `BlockHound.install(BlockHoundIntegration... integrations)` - same as `BlockHound.install()`, but adds user-provided integrations to the list. | ||
1. `BlockHound.builder().install()` - will create a **new** builder, **without** discovering any integrations. | ||
You may install them manually by using `BlockHound.builder().with(new MyIntegration()).install()`. | ||
|
||
## Marking more methods as blocking | ||
* `Builder#markAsBlocking(Class clazz, String methodName, String signature)` | ||
* `Builder#markAsBlocking(String className, String methodName, String signature)` | ||
|
||
Example: | ||
```java | ||
builder.markAsBlocking("com.example.NativeHelper", "doSomethingBlocking", "(I)V"); | ||
``` | ||
|
||
Note that the `signature` argument is | ||
[JVM's notation for the method signature](https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html#wp276). | ||
|
||
## (Dis-)allowing blocking calls inside methods | ||
* `Builder#allowBlockingCallsInside(String className, String methodName)` | ||
* `Builder#disallowBlockingCallsInside(String className, String methodName)` | ||
|
||
Example: | ||
|
||
This will allow blocking method calls inside `Logger#callAppenders` down the callstack: | ||
```java | ||
builder.allowBlockingCallsInside( | ||
"ch.qos.logback.classic.Logger", | ||
"callAppenders" | ||
); | ||
``` | ||
|
||
While this disallows blocking calls unless there is an allowed method down the callstack: | ||
```java | ||
builder.disallowBlockingCallsInside( | ||
"reactor.core.publisher.Flux", | ||
"subscribe" | ||
); | ||
``` | ||
|
||
## Custom blocking method callback | ||
* `Builder#blockingMethodCallback(Consumer<BlockingMethod> consumer)` | ||
|
||
By default, BlockHound will throw an error when it detects a blocking call. | ||
But you can implement your own logic by setting a callback. | ||
|
||
Example: | ||
```java | ||
builder.blockingMethodCallback(it -> { | ||
new Error(it.toString()).printStackTrace(); | ||
}); | ||
``` | ||
Here we dump the stacktrace instead of throwing the error, so that we do not alter an execution of the code. | ||
|
||
## Custom non-blocking thread predicate | ||
* `Builder#nonBlockingThreadPredicate(Function<Predicate<Thread>, Predicate<Thread>> predicate)` | ||
|
||
If you integrate with exotic technologies, or implement your own thread pooling, | ||
you might want to mark those threads as non-blocking. Example: | ||
```java | ||
builder.nonBlockingThreadPredicate(current -> { | ||
return current.or(it -> it.getName().contains("my-thread-")) | ||
}); | ||
``` | ||
|
||
⚠️ **Warning:** do not ignore the `current` predicate unless you're absolutely sure you know what you're doing. | ||
Other integrations will not work if you override it instead of using `Predicate#or`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# How it works | ||
|
||
BlockHound is a Java Agent with a JNI helper. | ||
|
||
It instruments the pre-defined set of blocking methods (see [customization](customization.md)) | ||
in the JVM and adds a special check (via JNI method) whether current thread is blocking or not before calling the callback. | ||
|
||
## Blocking Java method detection | ||
To detect blocking Java methods, BlockHound alters the bytecode of a method and adds the following line at the beginning of the method's body: | ||
```java | ||
// java.net.Socket | ||
public void connect(SocketAddress endpoint, int timeout) { | ||
reactor.BlockHoundRuntime.checkBlocking( | ||
"java.net.Socket", | ||
"connect", | ||
/*method modifiers*/ | ||
); | ||
``` | ||
|
||
`checkBlocking` will delegate to the JNI helper and maybe call the "blocking method detected" callback. | ||
|
||
The arguments are passed to the callback, but not used in the "blocking call" decision making. | ||
|
||
## Blocking JVM native method detection | ||
Since native methods in JVM can't be instrumented (they have no body), we use JVM's native method instrumentation technique. | ||
|
||
Consider the following blocking method: | ||
```java | ||
// java.lang.Thread | ||
public static native void sleep(long millis); | ||
``` | ||
|
||
The method is public and we can't instrument the wrapping Java method. Instead, we relocate the old native method: | ||
```java | ||
public static native void $$BlockHound$$_sleep(long millis); | ||
``` | ||
Then we create a new Java method, with exactly same signature as the old one, delegating to the old implementation: | ||
```java | ||
public static void sleep(long millis) { | ||
$$BlockHound$$_sleep(millis); | ||
} | ||
``` | ||
As you can see, the cost of such instrumentation is minimal and only adds 1 hop to the original method. | ||
Now, we add the blocking call detection, the same way as we do it with Java methods: | ||
```java | ||
public static void sleep(long millis) { | ||
reactor.BlockHoundRuntime.checkBlocking( | ||
"java.lang.Thread", | ||
"sleep", | ||
/*method modifiers*/ | ||
); | ||
$$BlockHound$$_sleep(millis); | ||
} | ||
``` | ||
## Blocking call decision | ||
For performance reasons, part of it is implemented in C++ and executed with JNI: | ||
1. First, it checks if a current thread is "tagged" already. If not, it creates a tag and calls user-provided predicate | ||
(see [customization](customization.md)) to mark it as either blocking or non-blocking. | ||
2. Then, it iterates the stack trace until it finds a pre-marked method (see [customization](customization.md)), and returns a boolean where: | ||
- `true` means "this method is not supposed to call blocking methods" | ||
- `false` means "this method may have a blocking call down the callstack" (think SLF4J logger writing something | ||
to the console with a blocking `OutputSteam#write` method). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# Quick start | ||
|
||
## Getting it | ||
Download it from repo.spring.io or Maven Central repositories (stable releases only): | ||
|
||
```groovy | ||
repositories { | ||
maven { url 'http://repo.spring.io/snapshot' } | ||
} | ||
dependencies { | ||
testCompile 'io.projectreactor:blockhound:$LATEST_SNAPSHOT' | ||
} | ||
``` | ||
Where `$LATEST_SNAPSHOT` is: | ||
![](https://img.shields.io/maven-metadata/v/https/repo.spring.io/snapshot/io/projectreactor/blockhound/maven-metadata.xml.svg) | ||
|
||
## Installation | ||
BlockHound is a JVM agent. You need to "install" it before it starts detecting the issues: | ||
```java | ||
BlockHound.install(); | ||
``` | ||
|
||
On install, it will discover all known integrations (see [writing custom integrations](custom_integrations.md) for details) | ||
and perform a one time instrumentation (see [how it works](how_it_works.md)). | ||
|
||
The best place to put this line is before *any* code gets executed, e.g. `@BeforeClass`, or `static {}` block, or test listener. | ||
The method is idempotent, you can call it multiple times. | ||
|
||
**NB:** it is highly recommended to add a dummy test with a well-known blocking call to ensure that it installed correctly. | ||
Something like this will work: | ||
```java | ||
Mono.delay(Duration.ofMillis(1)) | ||
.doOnNext(it -> { | ||
try { | ||
Thread.sleep(10); | ||
} | ||
catch (InterruptedException e) { | ||
throw new RuntimeException(e); | ||
} | ||
}) | ||
.block(); // should throw an exception about Thread.sleep | ||
``` | ||
|
||
## What's Next? | ||
You can further customize Blockhound's behavior, see [customization](customization.md). |