diff --git a/agent/src/main/java/reactor/blockhound/BlockHound.java b/agent/src/main/java/reactor/blockhound/BlockHound.java index abdefc2..34a6e56 100644 --- a/agent/src/main/java/reactor/blockhound/BlockHound.java +++ b/agent/src/main/java/reactor/blockhound/BlockHound.java @@ -62,7 +62,7 @@ */ public class BlockHound { - static final String PREFIX = "$$BlockHound$$_"; + static final String PREFIX = "$$BlockHound$$_"; private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); @@ -120,6 +120,10 @@ public TypePool typePool(ClassFileLocator classFileLocator, ClassLoader classLoa } } + /** + * A builder for configuration of the BlockHound java agent, passed to each {@link BlockHoundIntegration}'s + * {@link BlockHoundIntegration#applyTo(Builder) applyTo} method when {@link BlockHound#install(BlockHoundIntegration...) installing}. + */ public static class Builder { private final Map>> blockingMethods = new HashMap>>() {{ @@ -254,6 +258,45 @@ public static class Builder { private Predicate dynamicThreadPredicate = t -> false; + /** + * Set up a class-specific method allowance. See {@link Builder#allowBlockingCallsInside(String)}. + *

+ * Note that constructors are currently not supported as exception-throwing constructors cannot + * be instrumented by ByteBuddy (see https://github.com/reactor/BlockHound/issues/174). + */ + public class BuilderAllowSpec { + + final String className; + + private BuilderAllowSpec(String className) { + this.className = className; + } + + /** + * Allow blocking calls inside the static initializer block of the currently configured + * class and return to the {@link Builder}. + * + * @return the {@link Builder} + */ + public Builder forStaticInitializer() { + return Builder.this.allowBlockingCallsInside(this.className, ""); + } + + /** + * Allow blocking calls inside the specified method(s) of the currently configured + * class, and return to the {@link Builder}. An empty array has no effect on the + * builder configuration. + * + * @return the {@link Builder} + */ + public Builder forMethods(String... methodNames) { + for (String methodName : methodNames) { + Builder.this.allowBlockingCallsInside(this.className, methodName); + } + return Builder.this; + } + } + /** * Marks provided method of the provided class as "blocking". * @@ -290,16 +333,32 @@ public Builder markAsBlocking(String className, String methodName, String signat /** * Allows blocking calls inside any method of a class with name identified by the provided className * and which name matches the provided methodName. + *

+ * A sub-builder is available to guide users towards more advanced cases like static initializers, + * see {@link #allowBlockingCallsInside(String)} and {@link BuilderAllowSpec}. * * @param className class' name (e.g. "java.lang.Thread") * @param methodName a method name * @return this + * @see #allowBlockingCallsInside(String) + * @see BuilderAllowSpec */ public Builder allowBlockingCallsInside(String className, String methodName) { allowances.computeIfAbsent(className, __ -> new HashMap<>()).put(methodName, true); return this; } + /** + * Configure allowed blocking calls inside methods of a class with name identified by the provided className + * and methods identified by the {@link BuilderAllowSpec} chained call. + * + * @param className class' name (e.g. "java.lang.Thread") + * @return a sub-step of the builder as a {@link BuilderAllowSpec} + */ + public BuilderAllowSpec allowBlockingCallsInside(String className) { + return new BuilderAllowSpec(className); + } + /** * Disallows blocking calls inside any method of a class with name identified by the provided className * and which name matches the provided methodName. diff --git a/docs/customization.md b/docs/customization.md index 8efdb38..716e944 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -40,6 +40,14 @@ builder.disallowBlockingCallsInside( ); ``` +This will allow blocking method calls inside the static initializer block of `MyClass` down the callstack: +```java +builder.allowBlockingCallsInside( + "com.example.MyClass", + BlockHound.STATIC_INITIALIZER +); +``` + ## Custom blocking method callback * `Builder#blockingMethodCallback(Consumer consumer)` diff --git a/example/src/test/java/com/example/BlockingAllowSpecTest.java b/example/src/test/java/com/example/BlockingAllowSpecTest.java new file mode 100644 index 0000000..a063034 --- /dev/null +++ b/example/src/test/java/com/example/BlockingAllowSpecTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2020-Present Pivotal Software Inc, All Rights Reserved. + * + * Licensed 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 + * + * https://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 com.example; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import reactor.blockhound.BlockHound; +import reactor.blockhound.BlockingOperationError; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +public class BlockingAllowSpecTest { + + static { + BlockHound.install(b -> b + // class A + .allowBlockingCallsInside(BlockingClassA.class.getName()).forStaticInitializer() + .allowBlockingCallsInside(BlockingClassA.class.getName()).forMethods("block") + // then B + .allowBlockingCallsInside(BlockingClassB.class.getName()).forMethods("block1", "block2") + ); + } + + @Test + public void shouldInstrumentBlockingClassA() { + Mono.fromCallable(BlockingClassA::new) + .map(BlockingClassA::block) + .subscribeOn(Schedulers.parallel()) + .block(); + } + + @Test + public void shouldInstrumentBlockingClassB() { + Mono.fromCallable(() -> { + BlockingClassB b = new BlockingClassB(); + b.block1(); + b.block2(); + return b; + }).subscribeOn(Schedulers.parallel()).block(); + } + + @Test + public void usingAllowSpecWithoutCallingMethodIsIgnored() { + //getting the BuilderAllowSpec and not acting on it DOESN'T equate to allowing any method: it does nothing + BlockHound.install(b -> b.allowBlockingCallsInside(BlockingClassC.class.getName())); + + Mono mono = Mono.fromCallable(BlockingClassC::new) + .publishOn(Schedulers.parallel()) + .map(BlockingClassC::block1); + + Assertions.assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(mono::block) + .havingCause() + .isInstanceOf(BlockingOperationError.class) + .withMessage("Blocking call! java.lang.Thread.yield") + .withStackTraceContaining("at com.example.BlockingAllowSpecTest$BlockingClassC.block1"); + } + + static class BlockingClassA { + static { + try { + Thread.sleep(0); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + String block() { + Thread.yield(); + return "hello A"; + } + } + + static class BlockingClassB { + + void block1() { + try { + Thread.sleep(0); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + void block2() { + Thread.yield(); + } + } + + static class BlockingClassC { + + String block1() { + Thread.yield(); + return "hello C"; + } + } +} diff --git a/example/src/test/java/com/example/StaticInitTest.java b/example/src/test/java/com/example/StaticInitTest.java index 77d9d6f..f69006e 100644 --- a/example/src/test/java/com/example/StaticInitTest.java +++ b/example/src/test/java/com/example/StaticInitTest.java @@ -17,6 +17,7 @@ package com.example; import org.junit.Test; + import reactor.blockhound.BlockHound; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @@ -24,9 +25,7 @@ public class StaticInitTest { static { - BlockHound.install(b -> { - b.allowBlockingCallsInside(ClassWithStaticInit.class.getName(), ""); - }); + BlockHound.install(b -> b.allowBlockingCallsInside(ClassWithStaticInit.class.getName()).forStaticInitializer()); } @Test