Skip to content

Commit

Permalink
Initial documentation (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
bsideup committed Feb 1, 2019
1 parent 7eea53b commit 7c6d299
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Expand Up @@ -11,3 +11,8 @@ build/
.vscode/

cmake-build-*/


.project
.classpath
bin/
12 changes: 4 additions & 8 deletions README.md
Expand Up @@ -3,7 +3,7 @@
[![Travis CI](https://travis-ci.org/reactor/BlockHound.svg?branch=master)](https://travis-ci.org/reactor/BlockHound)
[![](https://img.shields.io/maven-metadata/v/https/repo.spring.io/snapshot/io/projectreactor/blockhound/maven-metadata.xml.svg)](https://repo.spring.io/snapshot/io/projectreactor/blockhound/)

Java agent to detect blocking calls from Reactor's non-blocking threads.
Java agent to detect blocking calls from non-blocking threads.

## How it works
BlockHound will transparently instrument the JVM classes and intercept blocking calls (e.g. IO) if they are performed from threads marked as "non-blocking operations only" (ie. threads implementing Reactor's `NonBlocking` marker interface, like those started by `Schedulers.parallel()`). If and when this happens (but remember, this should never happen!:stuck_out_tongue_winking_eye:), an error will be thrown. Here is an example:
Expand Down Expand Up @@ -33,7 +33,7 @@ Note that it points to the exact place where the blocking call got triggered. In

## Getting it

Download it from repo.spring.io or Maven Central repositories (stable releases only):
Download it from Maven Central repositories (stable releases only) or repo.spring.io:

```groovy
repositories {
Expand All @@ -47,12 +47,8 @@ dependencies {
Where `$LATEST_SNAPSHOT` is:
![](https://img.shields.io/maven-metadata/v/https/repo.spring.io/snapshot/io/projectreactor/blockhound/maven-metadata.xml.svg)

BlockHound is a JVM agent. You need to "install" it before it starts detecting the issues:
```java
BlockHound.install();
```

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.
# Quick Start
See [the docs](./docs/README.md).

-------------------------------------
_Licensed under [Apache Software License 2.0](www.apache.org/licenses/LICENSE-2.0)_
Expand Down
12 changes: 6 additions & 6 deletions agent/src/main/java/reactor/BlockHound.java
Expand Up @@ -153,7 +153,7 @@ public static class Builder {
throw new Error(String.format("Blocking call! %s", method));
};

private Predicate<Thread> blockingThreadPredicate = t -> false;
private Predicate<Thread> threadPredicate = t -> false;

public Builder markAsBlocking(Class clazz, String methodName, String signature) {
return markAsBlocking(clazz.getCanonicalName(), methodName, signature);
Expand Down Expand Up @@ -191,8 +191,8 @@ public Builder blockingMethodCallback(Consumer<BlockingMethod> consumer) {
return this;
}

public Builder blockingThreadPredicate(Function<Predicate<Thread>, Predicate<Thread>> predicate) {
this.blockingThreadPredicate = predicate.apply(this.blockingThreadPredicate);
public Builder nonBlockingThreadPredicate(Function<Predicate<Thread>, Predicate<Thread>> predicate) {
this.threadPredicate = predicate.apply(this.threadPredicate);
return this;
}

Expand Down Expand Up @@ -268,9 +268,9 @@ public void install() {
onBlockingMethod.accept(new BlockingMethod(className, methodName, modifiers));
});

Field blockingThreadPredicateField = runtimeClass.getDeclaredField("blockingThreadPredicate");
blockingThreadPredicateField.setAccessible(true);
blockingThreadPredicateField.set(null, blockingThreadPredicate);
Field threadPredicateField = runtimeClass.getDeclaredField("threadPredicate");
threadPredicateField.setAccessible(true);
threadPredicateField.set(null, threadPredicate);
}
catch (Throwable e) {
throw new RuntimeException(e);
Expand Down
4 changes: 2 additions & 2 deletions agent/src/main/java/reactor/BlockHoundRuntime.java
Expand Up @@ -50,7 +50,7 @@ public class BlockHoundRuntime {
private static volatile Consumer<Object[]> blockingMethodConsumer;

@SuppressWarnings("unused")
private static volatile Predicate<Thread> blockingThreadPredicate;
private static volatile Predicate<Thread> threadPredicate;

@SuppressWarnings("unused")
public static void checkBlocking(String className, String methodName, int modifiers) {
Expand All @@ -62,7 +62,7 @@ public static void checkBlocking(String className, String methodName, int modifi
@SuppressWarnings("unused")
private static boolean isBlockingThread(Thread thread) {
try {
return blockingThreadPredicate.test(thread);
return threadPredicate.test(thread);
}
catch (Error e) {
e.printStackTrace();
Expand Down
Expand Up @@ -36,7 +36,7 @@ public void applyTo(BlockHound.Builder builder) {
return;
}

builder.blockingThreadPredicate(current -> current.or(NonBlocking.class::isInstance));
builder.nonBlockingThreadPredicate(current -> current.or(NonBlocking.class::isInstance));

for (String className : new String[]{"Flux", "Mono", "ParallelFlux"}) {
builder.disallowBlockingCallsInside("reactor.core.publisher." + className, "subscribe");
Expand Down
Expand Up @@ -42,7 +42,7 @@ public void applyTo(BlockHound.Builder builder) {
: MarkerRunnable::new
);

builder.blockingThreadPredicate(current -> current.or(NonBlockingThread.class::isInstance));
builder.nonBlockingThreadPredicate(current -> current.or(NonBlockingThread.class::isInstance));

// TODO more places?
builder.disallowBlockingCallsInside(MarkerRunnable.class.getName(), "run");
Expand Down
5 changes: 5 additions & 0 deletions docs/README.md
@@ -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)
40 changes: 40 additions & 0 deletions docs/custom_integrations.md
@@ -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
69 changes: 69 additions & 0 deletions docs/customization.md
@@ -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`.
66 changes: 66 additions & 0 deletions docs/how_it_works.md
@@ -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).
46 changes: 46 additions & 0 deletions docs/quick_start.md
@@ -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).

0 comments on commit 7c6d299

Please sign in to comment.