supplier) {
- return decorateSupplier(this, supplier);
- }
-
/**
* Returns a supplier which submits a value-returning task for execution and
* returns a CompletionStage representing the asynchronous computation of the task.
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkhead.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkhead.java
new file mode 100644
index 0000000000..1922aa6a93
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkhead.java
@@ -0,0 +1,722 @@
+/*
+ *
+ * Copyright 2019: Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.adaptive;
+
+import io.github.resilience4j.bulkhead.Bulkhead;
+import io.github.resilience4j.bulkhead.BulkheadFullException;
+import io.github.resilience4j.bulkhead.adaptive.internal.AdaptiveBulkheadStateMachine;
+import io.github.resilience4j.bulkhead.event.*;
+import io.github.resilience4j.core.EventConsumer;
+import io.github.resilience4j.core.EventPublisher;
+import io.github.resilience4j.core.functions.CheckedConsumer;
+import io.github.resilience4j.core.functions.CheckedFunction;
+import io.github.resilience4j.core.functions.CheckedRunnable;
+import io.github.resilience4j.core.functions.CheckedSupplier;
+import io.github.resilience4j.core.functions.OnceConsumer;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.concurrent.*;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * A Bulkhead instance is thread-safe can be used to decorate multiple requests.
+ *
+ * A {@link AdaptiveBulkhead} represent an entity limiting the amount of parallel operations. It
+ * does not assume nor does it mandate usage of any particular concurrency and/or io model. These
+ * details are left for the client to manage. This bulkhead, depending on the underlying
+ * concurrency/io model can be used to shed load, and, where it makes sense, limit resource use
+ * (i.e. limit amount of threads/actors involved in a particular flow, etc).
+ *
+ * In order to execute an operation protected by this bulkhead, a permission must be obtained by
+ * calling {@link AdaptiveBulkhead#tryAcquirePermission()} ()} If the bulkhead is full, no
+ * additional operations will be permitted to execute until space is available.
+ *
+ * Once the operation is complete, regardless of the result (Success or Failure), client needs to
+ * call {@link AdaptiveBulkhead#onSuccess(long, TimeUnit)} or {@link AdaptiveBulkhead#onError(long,
+ * TimeUnit, Throwable)} in order to maintain integrity of internal bulkhead state which is handled
+ * by invoking the configured adaptive limit policy.
+ *
+ */
+public interface AdaptiveBulkhead {
+
+ /**
+ * Acquires a permission to execute a call, only if one is available at the time of invocation.
+ *
+ * @return {@code true} if a permission was acquired and {@code false} otherwise
+ */
+
+ boolean tryAcquirePermission();
+
+ /**
+ * Acquires a permission to execute a call, only if one is available at the time of invocation
+ *
+ * @throws BulkheadFullException when the Bulkhead is full and no further calls are permitted.
+ */
+ void acquirePermission();
+
+ /**
+ * Releases a permission and increases the number of available permits by one.
+ *
+ * Should only be used when a permission was acquired but not used. Otherwise use {@link
+ * AdaptiveBulkhead#onSuccess(long, TimeUnit)} to signal a completed call and release a
+ * permission.
+ */
+ void releasePermission();
+
+ /**
+ * Records a successful call and releases a permission.
+ */
+ void onSuccess(long startTime, TimeUnit durationUnit);
+
+ /**
+ * Records a failed call and releases a permission.
+ */
+ void onError(long startTime, TimeUnit durationUnit, Throwable throwable);
+
+ /**
+ * Returns the name of this bulkhead.
+ *
+ * @return the name of this bulkhead
+ */
+ String getName();
+
+ /**
+ * Returns the AdaptiveBulkheadConfig of this Bulkhead.
+ *
+ * @return bulkhead config
+ */
+ AdaptiveBulkheadConfig getBulkheadConfig();
+
+ /**
+ * Get the Metrics of this Bulkhead.
+ *
+ * @return the Metrics of this Bulkhead
+ */
+ Metrics getMetrics();
+
+ /**
+ * Returns an EventPublisher which subscribes to the reactive stream of
+ * BulkheadEvent/AdaptiveBulkheadEvent events and can be used to register event consumers.
+ *
+ * @return an AdaptiveEventPublisher
+ */
+ AdaptiveEventPublisher getEventPublisher();
+
+ /**
+ * Decorates and executes the decorated Supplier.
+ *
+ * @param supplier the original Supplier
+ * @param the type of results supplied by this supplier
+ * @return the result of the decorated Supplier.
+ */
+ default T executeSupplier(Supplier supplier) {
+ return decorateSupplier(this, supplier).get();
+ }
+
+ /**
+ * Decorates and executes the decorated Callable.
+ *
+ * @param callable the original Callable
+ * @param the result type of callable
+ * @return the result of the decorated Callable.
+ * @throws Exception if unable to compute a result
+ */
+ default T executeCallable(Callable callable) throws Exception {
+ return decorateCallable(this, callable).call();
+ }
+
+ /**
+ * Decorates and executes the decorated Runnable.
+ *
+ * @param runnable the original Runnable
+ */
+ default void executeRunnable(Runnable runnable) {
+ decorateRunnable(this, runnable).run();
+ }
+
+ /**
+ * Decorates and executes the decorated Supplier.
+ *
+ * @param checkedSupplier the original Supplier
+ * @param the type of results supplied by this supplier
+ * @return the result of the decorated Supplier.
+ * @throws Throwable if something goes wrong applying this function to the given arguments
+ */
+ default T executeCheckedSupplier(CheckedSupplier checkedSupplier) throws Throwable {
+ return decorateCheckedSupplier(this, checkedSupplier).get();
+ }
+
+ /**
+ * Decorates and executes the decorated CompletionStage.
+ *
+ * @param supplier the original CompletionStage
+ * @param the type of results supplied by this supplier
+ * @return the decorated CompletionStage.
+ */
+ default CompletionStage executeCompletionStage(Supplier> supplier) {
+ return decorateCompletionStage(this, supplier).get();
+ }
+
+ /**
+ * Returns a supplier which is decorated by a bulkhead.
+ *
+ * @param bulkhead the Bulkhead
+ * @param supplier the original supplier
+ * @param the type of results supplied by this supplier
+ * @return a supplier which is decorated by a Bulkhead.
+ */
+ static CheckedSupplier decorateCheckedSupplier(AdaptiveBulkhead bulkhead,
+ CheckedSupplier supplier) {
+ return () -> {
+ long start = 0;
+ boolean isFailed = false;
+ bulkhead.acquirePermission();
+ try {
+ start = System.currentTimeMillis();
+ return supplier.get();
+ } catch (Exception e) {
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ isFailed = true;
+ throw e;
+ } finally {
+ if (start != 0 && !isFailed) {
+ bulkhead.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a supplier which is decorated by a bulkhead.
+ *
+ * @param bulkhead the bulkhead
+ * @param supplier the original supplier
+ * @param the type of the returned CompletionStage's result
+ * @return a supplier which is decorated by a Bulkhead.
+ */
+ static Supplier> decorateCompletionStage(AdaptiveBulkhead bulkhead,
+ Supplier> supplier) {
+ return () -> {
+
+ final CompletableFuture promise = new CompletableFuture<>();
+
+ if (!bulkhead.tryAcquirePermission()) {
+ promise.completeExceptionally(
+ BulkheadFullException.createBulkheadFullException(bulkhead));
+ } else {
+ long start = System.currentTimeMillis();
+ try {
+ supplier.get()
+ .whenComplete(
+ (result, throwable) -> {
+ if (throwable != null) {
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, throwable);
+ promise.completeExceptionally(throwable);
+ } else {
+ bulkhead.onSuccess(System.currentTimeMillis() - start,
+ TimeUnit.MILLISECONDS);
+ promise.complete(result);
+ }
+ }
+ );
+ } catch (Exception e) {
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ promise.completeExceptionally(e);
+ }
+ }
+ return promise;
+ };
+ }
+
+ /**
+ * Returns a supplier of type Future which is decorated by a bulkhead. AdaptiveBulkhead will reserve permission until {@link Future#get()}
+ * or {@link Future#get(long, TimeUnit)} is evaluated even if the underlying call took less time to return. Any delays in evaluating
+ * future will result in holding of permission in the underlying Semaphore.
+ *
+ * @param bulkhead the bulkhead
+ * @param supplier the original supplier
+ * @param the type of the returned Future result
+ * @return a supplier which is decorated by a AdaptiveBulkhead.
+ */
+ static Supplier> decorateFuture(AdaptiveBulkhead bulkhead, Supplier> supplier) {
+ return () -> {
+ if (!bulkhead.tryAcquirePermission()) {
+ final CompletableFuture promise = new CompletableFuture<>();
+ promise.completeExceptionally(BulkheadFullException.createBulkheadFullException(bulkhead));
+ return promise;
+ }
+ long start = System.currentTimeMillis();
+ try {
+ return new BulkheadFuture(bulkhead, supplier.get());
+ } catch (Throwable e) {
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ throw e;
+ }
+ };
+ }
+
+ /**
+ * Returns a runnable which is decorated by a bulkhead.
+ *
+ * @param bulkhead the bulkhead
+ * @param runnable the original runnable
+ * @return a runnable which is decorated by a Bulkhead.
+ */
+ static CheckedRunnable decorateCheckedRunnable(AdaptiveBulkhead bulkhead,
+ CheckedRunnable runnable) {
+ return () -> {
+ long start = 0;
+ boolean isFailed = false;
+ bulkhead.acquirePermission();
+ try {
+ start = System.currentTimeMillis();
+ runnable.run();
+ } catch (Exception e) {
+ isFailed = true;
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ throw e;
+ } finally {
+ if (start != 0 && !isFailed) {
+ bulkhead.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a callable which is decorated by a bulkhead.
+ *
+ * @param bulkhead the bulkhead
+ * @param callable the original Callable
+ * @param the result type of callable
+ * @return a supplier which is decorated by a Bulkhead.
+ */
+ static Callable decorateCallable(AdaptiveBulkhead bulkhead, Callable callable) {
+ return () -> {
+ long start = 0;
+ boolean isFailed = false;
+ bulkhead.acquirePermission();
+ try {
+ start = System.currentTimeMillis();
+ return callable.call();
+ } catch (Exception e) {
+ isFailed = true;
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ throw e;
+ } finally {
+ if (start != 0 && !isFailed) {
+ bulkhead.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a supplier which is decorated by a bulkhead.
+ *
+ * @param bulkhead the bulkhead
+ * @param supplier the original supplier
+ * @param the type of results supplied by this supplier
+ * @return a supplier which is decorated by a Bulkhead.
+ */
+ static Supplier decorateSupplier(AdaptiveBulkhead bulkhead, Supplier supplier) {
+ return () -> {
+ long start = 0;
+ boolean isFailed = false;
+ bulkhead.acquirePermission();
+ try {
+ start = System.currentTimeMillis();
+ return supplier.get();
+ } catch (Exception e) {
+ isFailed = true;
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ throw e;
+ } finally {
+ if (start != 0 && !isFailed) {
+ bulkhead.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a consumer which is decorated by a bulkhead.
+ *
+ * @param bulkhead the bulkhead
+ * @param consumer the original consumer
+ * @param the type of the input to the consumer
+ * @return a consumer which is decorated by a Bulkhead.
+ */
+ static Consumer decorateConsumer(AdaptiveBulkhead bulkhead, Consumer consumer) {
+ return t -> {
+ long start = 0;
+ boolean failed = false;
+ bulkhead.acquirePermission();
+ try {
+ start = System.currentTimeMillis();
+ consumer.accept(t);
+ } catch (Exception e) {
+ failed = true;
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ throw e;
+ } finally {
+ if (start != 0 && !failed) {
+ bulkhead.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a consumer which is decorated by a bulkhead.
+ *
+ * @param bulkhead the bulkhead
+ * @param consumer the original consumer
+ * @param the type of the input to the consumer
+ * @return a consumer which is decorated by a Bulkhead.
+ */
+ static CheckedConsumer decorateCheckedConsumer(AdaptiveBulkhead bulkhead,
+ CheckedConsumer consumer) {
+ return t -> {
+ long start = 0;
+ boolean failed = false;
+ bulkhead.acquirePermission();
+ try {
+ start = System.currentTimeMillis();
+ consumer.accept(t);
+ } catch (Exception e) {
+ failed = true;
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ throw e;
+ } finally {
+ if (start != 0 && !failed) {
+ bulkhead.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a runnable which is decorated by a bulkhead.
+ *
+ * @param bulkhead the bulkhead
+ * @param runnable the original runnable
+ * @return a runnable which is decorated by a bulkhead.
+ */
+ static Runnable decorateRunnable(AdaptiveBulkhead bulkhead, Runnable runnable) {
+ return () -> {
+ long start = 0;
+ boolean failed = false;
+ bulkhead.acquirePermission();
+ try {
+ start = System.currentTimeMillis();
+ runnable.run();
+ } catch (Exception e) {
+ failed = true;
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ throw e;
+ } finally {
+ if (start != 0 && !failed) {
+ bulkhead.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a function which is decorated by a bulkhead.
+ *
+ * @param bulkhead the bulkhead
+ * @param function the original function
+ * @param the type of the input to the function
+ * @param the type of the result of the function
+ * @return a function which is decorated by a bulkhead.
+ */
+ static Function decorateFunction(AdaptiveBulkhead bulkhead,
+ Function function) {
+ return (T t) -> {
+ long start = 0;
+ boolean failed = false;
+ bulkhead.acquirePermission();
+ try {
+ start = System.currentTimeMillis();
+ return function.apply(t);
+ } catch (Exception e) {
+ failed = true;
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ throw e;
+ } finally {
+ if (start != 0 && !failed) {
+ bulkhead.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a function which is decorated by a bulkhead.
+ *
+ * @param bulkhead the bulkhead
+ * @param function the original function
+ * @param the type of the input to the function
+ * @param the type of the result of the function
+ * @return a function which is decorated by a bulkhead.
+ */
+ static CheckedFunction decorateCheckedFunction(AdaptiveBulkhead bulkhead,
+ CheckedFunction function) {
+ return (T t) -> {
+ long start = 0;
+ boolean failed = false;
+ bulkhead.acquirePermission();
+ try {
+ start = System.currentTimeMillis();
+ return function.apply(t);
+ } catch (Exception e) {
+ failed = true;
+ bulkhead.onError(start, TimeUnit.MILLISECONDS, e);
+ throw e;
+ } finally {
+ if (start != 0 && !failed) {
+ bulkhead.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
+ }
+ }
+ };
+ }
+
+ /**
+ * Create a Bulkhead with a default configuration.
+ *
+ * @param name the name of the bulkhead
+ * @return a Bulkhead instance
+ */
+ static AdaptiveBulkhead ofDefaults(String name) {
+ return new AdaptiveBulkheadStateMachine(name, AdaptiveBulkheadConfig.ofDefaults());
+ }
+
+ /**
+ * Creates a bulkhead with a custom configuration
+ *
+ * @param name the name of the bulkhead
+ * @param config a custom BulkheadConfig configuration
+ * @return a Bulkhead instance
+ */
+ static AdaptiveBulkhead of(String name, AdaptiveBulkheadConfig config) {
+ return new AdaptiveBulkheadStateMachine(name, config);
+ }
+
+ /**
+ * Creates a bulkhead with a custom configuration
+ *
+ * @param name the name of the bulkhead
+ * @param bulkheadConfigSupplier custom configuration supplier
+ * @return a Bulkhead instance
+ */
+ static AdaptiveBulkhead of(String name,
+ Supplier bulkheadConfigSupplier) {
+ return new AdaptiveBulkheadStateMachine(name, bulkheadConfigSupplier.get());
+ }
+
+ void transitionToCongestionAvoidance();
+
+ void transitionToSlowStart();
+
+ /**
+ * States of the AdaptiveBulkhead.
+ */
+ enum State {
+ /**
+ * A DISABLED adaptive bulkhead is not operating (no state transition, no events) and no
+ * limit changes.
+ */
+ // TODO not implemented
+ DISABLED(3, false),
+
+ SLOW_START(1, true),
+
+ CONGESTION_AVOIDANCE(2, true);
+
+ public final boolean allowPublish;
+ private final int order;
+
+ /**
+ * Order is a FIXED integer, it should be preserved regardless of the ordinal number of the
+ * enumeration. While a State.ordinal() does mostly the same, it is prone to changing the
+ * order based on how the programmer sets the enum. If more states are added the "order"
+ * should be preserved. For example, if there is a state inserted between CLOSED and
+ * HALF_OPEN (say FIXED_OPEN) then the order of HALF_OPEN remains at 2 and the new state
+ * takes 3 regardless of its order in the enum.
+ *
+ * @param order
+ * @param allowPublish
+ */
+ State(int order, boolean allowPublish) {
+ this.order = order;
+ this.allowPublish = allowPublish;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+ }
+
+ interface Metrics extends Bulkhead.Metrics {
+
+ /**
+ * Returns the current failure rate in percentage. If the number of measured calls is below
+ * the minimum number of measured calls, it returns -1.
+ *
+ * @return the failure rate in percentage
+ */
+ float getFailureRate();
+
+ /**
+ * Returns the current percentage of calls which were slower than a certain threshold. If
+ * the number of measured calls is below the minimum number of measured calls, it returns
+ * -1.
+ *
+ * @return the failure rate in percentage
+ */
+ float getSlowCallRate();
+
+ /**
+ * Returns the current total number of calls which were slower than a certain threshold.
+ *
+ * @return the current total number of calls which were slower than a certain threshold
+ */
+ int getNumberOfSlowCalls();
+
+ /**
+ * Returns the current number of successful calls which were slower than a certain
+ * threshold.
+ *
+ * @return the current number of successful calls which were slower than a certain threshold
+ */
+ int getNumberOfSlowSuccessfulCalls();
+
+ /**
+ * Returns the current number of failed calls which were slower than a certain threshold.
+ *
+ * @return the current number of failed calls which were slower than a certain threshold
+ */
+ int getNumberOfSlowFailedCalls();
+
+ /**
+ * Returns the current total number of buffered calls in the sliding window.
+ *
+ * @return he current total number of buffered calls in the sliding window
+ */
+ int getNumberOfBufferedCalls();
+
+ /**
+ * Returns the current number of failed buffered calls in the sliding window.
+ *
+ * @return the current number of failed buffered calls in the sliding window
+ */
+ int getNumberOfFailedCalls();
+
+ /**
+ * Returns the current number of successful buffered calls in the sliding window.
+ *
+ * @return the current number of successful buffered calls in the sliding window
+ */
+ int getNumberOfSuccessfulCalls();
+
+ void resetRecords();
+ }
+
+ /**
+ * An EventPublisher which can be used to register event consumers.
+ */
+ interface AdaptiveEventPublisher extends EventPublisher {
+
+ // TODO Maybe we can replace these 2 events by 1 BulkheadOnLimitChangedEvent(oldValue, newValue)
+ EventPublisher onLimitIncreased(EventConsumer eventConsumer);
+
+ EventPublisher onLimitDecreased(EventConsumer eventConsumer);
+
+ EventPublisher onSuccess(EventConsumer eventConsumer);
+
+ EventPublisher onError(EventConsumer eventConsumer);
+
+ EventPublisher onIgnoredError(EventConsumer eventConsumer);
+
+ EventPublisher onStateTransition(
+ EventConsumer eventConsumer);
+
+ }
+
+ /**
+ * This class decorates future with AdaptiveBulkhead functionality around invocation.
+ *
+ * @param of return type
+ */
+ final class BulkheadFuture implements Future {
+ final private Future future;
+ final private OnceConsumer onceToBulkhead;
+
+ BulkheadFuture(AdaptiveBulkhead bulkhead, Future future) {
+ Objects.requireNonNull(future, "Non null Future is required to decorate");
+ this.onceToBulkhead = OnceConsumer.of(bulkhead);
+ this.future = future;
+
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return future.cancel(mayInterruptIfRunning);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return future.isCancelled();
+ }
+
+ @Override
+ public boolean isDone() {
+ return future.isDone();
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ long start = System.currentTimeMillis();
+ try {
+ return future.get();
+ } finally {
+ // TODO onError?
+ onceToBulkhead.applyOnce(b ->
+ b.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ long start = System.currentTimeMillis();
+ try {
+ return future.get(timeout, unit);
+ } finally {
+ // TODO onError?
+ onceToBulkhead.applyOnce(b ->
+ b.onSuccess(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS));
+ }
+ }
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadConfig.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadConfig.java
new file mode 100644
index 0000000000..d31d2de89c
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadConfig.java
@@ -0,0 +1,517 @@
+/*
+ *
+ * Copyright 2019: Bohdan Storozhuk, Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.adaptive;
+
+import java.time.Duration;
+import java.util.function.Predicate;
+
+import io.github.resilience4j.bulkhead.adaptive.internal.AdaptiveBulkheadStateMachine;
+import io.github.resilience4j.core.lang.NonNull;
+import io.github.resilience4j.core.lang.Nullable;
+import io.github.resilience4j.core.predicate.PredicateCreator;
+
+/**
+ * A {@link AdaptiveBulkheadConfig} configures a adaptation capabilities of {@link AdaptiveBulkheadStateMachine}
+ */
+public class AdaptiveBulkheadConfig {
+
+ private static final int DEFAULT_MAX_CONCURRENT_CALLS = 25;
+ private static final int DEFAULT_MIN_CONCURRENT_CALLS = 2;
+ private static final int DEFAULT_INITIAL_CONCURRENT_CALLS = DEFAULT_MIN_CONCURRENT_CALLS;
+ private static final Duration DEFAULT_MAX_WAIT_DURATION = Duration.ofSeconds(0);
+
+ private static final float DEFAULT_FAILURE_RATE_THRESHOLD_PERCENTAGE = 50.0f;
+ private static final float DEFAULT_SLOW_CALL_RATE_THRESHOLD_PERCENTAGE = 50.0f;
+ private static final int DEFAULT_SLIDING_WINDOW_SIZE = 100;
+ private static final long DEFAULT_SLOW_CALL_DURATION_THRESHOLD_SECONDS = 5;
+ private static final int DEFAULT_MINIMUM_NUMBER_OF_CALLS = 100;
+ private static final boolean DEFAULT_WRITABLE_STACK_TRACE_ENABLED = true;
+ private static final SlidingWindowType DEFAULT_SLIDING_WINDOW_TYPE = SlidingWindowType.COUNT_BASED;
+ // The default exception predicate counts all exceptions as failures.
+ private static final Predicate DEFAULT_RECORD_EXCEPTION_PREDICATE = throwable -> true;
+ // The default exception predicate ignores no exceptions.
+ private static final Predicate DEFAULT_IGNORE_EXCEPTION_PREDICATE = throwable -> false;
+ private static final int DEFAULT_INCREASE_SUMMAND = 1;
+ private static final float DEFAULT_INCREASE_MULTIPLIER = 2f;
+ private static final float DEFAULT_DECREASE_MULTIPLIER = 0.5f;
+
+ @SuppressWarnings("unchecked")
+ private Class extends Throwable>[] recordExceptions = new Class[0];
+ @SuppressWarnings("unchecked")
+ private Class extends Throwable>[] ignoreExceptions = new Class[0];
+ @NonNull
+ private Predicate recordExceptionPredicate = DEFAULT_RECORD_EXCEPTION_PREDICATE;
+ @NonNull
+ private Predicate ignoreExceptionPredicate = DEFAULT_IGNORE_EXCEPTION_PREDICATE;
+ private int minimumNumberOfCalls = DEFAULT_MINIMUM_NUMBER_OF_CALLS;
+ private boolean writableStackTraceEnabled = DEFAULT_WRITABLE_STACK_TRACE_ENABLED;
+ private float failureRateThreshold = DEFAULT_FAILURE_RATE_THRESHOLD_PERCENTAGE;
+ private int slidingWindowSize = DEFAULT_SLIDING_WINDOW_SIZE;
+ private SlidingWindowType slidingWindowType = DEFAULT_SLIDING_WINDOW_TYPE;
+ private float slowCallRateThreshold = DEFAULT_SLOW_CALL_RATE_THRESHOLD_PERCENTAGE;
+ private Duration slowCallDurationThreshold = Duration
+ .ofSeconds(DEFAULT_SLOW_CALL_DURATION_THRESHOLD_SECONDS);
+ private int minConcurrentCalls = DEFAULT_MIN_CONCURRENT_CALLS;
+ private int initialConcurrentCalls = DEFAULT_INITIAL_CONCURRENT_CALLS;
+ private int maxConcurrentCalls = DEFAULT_MAX_CONCURRENT_CALLS;
+ private int increaseSummand = DEFAULT_INCREASE_SUMMAND;
+ private float decreaseMultiplier = DEFAULT_DECREASE_MULTIPLIER;
+ private Duration maxWaitDuration = DEFAULT_MAX_WAIT_DURATION;
+
+ private AdaptiveBulkheadConfig() {
+ }
+
+ public Predicate getRecordExceptionPredicate() {
+ return recordExceptionPredicate;
+ }
+
+ public Predicate getIgnoreExceptionPredicate() {
+ return ignoreExceptionPredicate;
+ }
+
+ public int getMinimumNumberOfCalls() {
+ return minimumNumberOfCalls;
+ }
+
+ public float getFailureRateThreshold() {
+ return failureRateThreshold;
+ }
+
+ public int getSlidingWindowSize() {
+ return slidingWindowSize;
+ }
+
+ @NonNull
+ public SlidingWindowType getSlidingWindowType() {
+ return slidingWindowType;
+ }
+
+ public float getSlowCallRateThreshold() {
+ return slowCallRateThreshold;
+ }
+
+ public Duration getSlowCallDurationThreshold() {
+ return slowCallDurationThreshold;
+ }
+
+ public boolean isWritableStackTraceEnabled() {
+ return writableStackTraceEnabled;
+ }
+
+ public int getMinConcurrentCalls() {
+ return minConcurrentCalls;
+ }
+
+ public int getInitialConcurrentCalls() {
+ return initialConcurrentCalls;
+ }
+
+ public int getMaxConcurrentCalls() {
+ return maxConcurrentCalls;
+ }
+
+ public int getIncreaseSummand() {
+ return increaseSummand;
+ }
+
+ public float getDecreaseMultiplier() {
+ return decreaseMultiplier;
+ }
+
+ public float getIncreaseMultiplier() {
+ // TODO
+ return DEFAULT_INCREASE_MULTIPLIER;
+ }
+
+ public Duration getMaxWaitDuration() {
+ return maxWaitDuration;
+ }
+
+ public enum SlidingWindowType {
+ TIME_BASED, COUNT_BASED
+ }
+
+ /**
+ * Returns a builder to create a custom AdaptiveBulkheadConfig.
+ *
+ * @return a {@link AdaptiveBulkheadConfig.Builder}
+ */
+ public static Builder from(AdaptiveBulkheadConfig baseConfig) {
+ return AdaptiveBulkheadConfig.builder(baseConfig);
+ }
+
+ /**
+ * Creates a default Bulkhead configuration.
+ *
+ * @return a default Bulkhead configuration.
+ */
+ public static AdaptiveBulkheadConfig ofDefaults() {
+ return AdaptiveBulkheadConfig.custom().build();
+ }
+
+ /**
+ * Returns a builder to create a custom AdaptiveBulkheadConfig.
+ *
+ * @return a {@link AdaptiveBulkheadConfig.Builder}
+ */
+ public static Builder custom() {
+ return new Builder();
+ }
+
+ /**
+ * Returns a builder to create a custom AdaptiveBulkheadConfig.
+ *
+ * @return a {@link AdaptiveBulkheadConfig.Builder}
+ */
+ public static Builder builder(AdaptiveBulkheadConfig bulkheadConfig) {
+ return new Builder(bulkheadConfig);
+ }
+
+ public static class Builder {
+
+ private float failureRateThreshold = DEFAULT_FAILURE_RATE_THRESHOLD_PERCENTAGE;
+ private int minimumNumberOfCalls = DEFAULT_MINIMUM_NUMBER_OF_CALLS;
+ private int slidingWindowSize = DEFAULT_SLIDING_WINDOW_SIZE;
+ private SlidingWindowType slidingWindowType = DEFAULT_SLIDING_WINDOW_TYPE;
+ private float slowCallRateThreshold = DEFAULT_SLOW_CALL_RATE_THRESHOLD_PERCENTAGE;
+ private Duration slowCallDurationThreshold = Duration
+ .ofSeconds(DEFAULT_SLOW_CALL_DURATION_THRESHOLD_SECONDS);
+ private boolean writableStackTraceEnabled = DEFAULT_WRITABLE_STACK_TRACE_ENABLED;
+ private int minConcurrentCalls = DEFAULT_MIN_CONCURRENT_CALLS;
+ private int maxConcurrentCalls = DEFAULT_MAX_CONCURRENT_CALLS;
+ private int initialConcurrentCalls = DEFAULT_INITIAL_CONCURRENT_CALLS;
+ private int increaseSummand = DEFAULT_INCREASE_SUMMAND;
+ private float decreaseMultiplier = DEFAULT_DECREASE_MULTIPLIER;
+ private Duration maxWaitDuration = DEFAULT_MAX_WAIT_DURATION;
+ @Nullable
+ private Predicate recordExceptionPredicate;
+ @Nullable
+ private Predicate ignoreExceptionPredicate;
+ @SuppressWarnings("unchecked")
+ private Class extends Throwable>[] recordExceptions = new Class[0];
+ @SuppressWarnings("unchecked")
+ private Class extends Throwable>[] ignoreExceptions = new Class[0];
+
+ private Builder() {
+ }
+
+ private Builder(AdaptiveBulkheadConfig baseConfig) {
+ this.slidingWindowSize = baseConfig.slidingWindowSize;
+ this.slidingWindowType = baseConfig.slidingWindowType;
+ this.minimumNumberOfCalls = baseConfig.minimumNumberOfCalls;
+ this.failureRateThreshold = baseConfig.failureRateThreshold;
+ this.ignoreExceptions = baseConfig.ignoreExceptions;
+ this.recordExceptions = baseConfig.recordExceptions;
+ this.recordExceptionPredicate = baseConfig.recordExceptionPredicate;
+ this.writableStackTraceEnabled = baseConfig.writableStackTraceEnabled;
+ this.slowCallRateThreshold = baseConfig.slowCallRateThreshold;
+ this.slowCallDurationThreshold = baseConfig.slowCallDurationThreshold;
+ this.minConcurrentCalls = baseConfig.minConcurrentCalls;
+ this.maxConcurrentCalls = baseConfig.maxConcurrentCalls;
+ this.initialConcurrentCalls = baseConfig.initialConcurrentCalls;
+ this.increaseSummand = baseConfig.increaseSummand;
+ this.decreaseMultiplier = baseConfig.decreaseMultiplier;
+ }
+
+ /**
+ * Configures a threshold in percentage. The AdaptiveBulkhead considers a call as slow when
+ * the call duration is greater than {@link #slowCallDurationThreshold(Duration)}.
+ *
+ *
+ * The threshold must be greater than 0 and not greater than 100. Default value is 100
+ * percentage which means that all recorded calls must be slower than {@link
+ * #slowCallDurationThreshold(Duration)}.
+ *
+ * @param slowCallRateThreshold the slow calls threshold in percentage
+ * @return the AdaptiveBulkheadConfig.Builder
+ * @throws IllegalArgumentException if {@code slowCallRateThreshold <= 0 ||
+ * slowCallRateThreshold > 100}
+ */
+ public Builder slowCallRateThreshold(float slowCallRateThreshold) {
+ if (slowCallRateThreshold <= 0 || slowCallRateThreshold > 100) {
+ throw new IllegalArgumentException(
+ "slowCallRateThreshold must be between 1 and 100");
+ }
+ this.slowCallRateThreshold = slowCallRateThreshold;
+ return this;
+ }
+
+ /**
+ * Configures the duration threshold above which calls are considered as slow and increase
+ * the slow calls percentage. Default value is 60 seconds.
+ *
+ * @param slowCallDurationThreshold the duration above which calls are considered as slow
+ * @return the AdaptiveBulkheadConfig.Builder
+ * @throws IllegalArgumentException if {@code slowCallDurationThreshold.toNanos() < 1}
+ */
+ public Builder slowCallDurationThreshold(Duration slowCallDurationThreshold) {
+ if (slowCallDurationThreshold.toNanos() < 1) {
+ throw new IllegalArgumentException(
+ "slowCallDurationThreshold must be at least 1[ns]");
+ }
+ this.slowCallDurationThreshold = slowCallDurationThreshold;
+ return this;
+ }
+
+ /**
+ * Configures the size of the sliding window which is used to record the outcome of calls.
+ * {@code slidingWindowSize} configures the size of the sliding window.
+ *
+ * The {@code slidingWindowSize} must be greater than 0.
+ *
+ * Default slidingWindowSize is 100.
+ *
+ * @param slidingWindowSize the size of the sliding window when the AdaptiveBulkhead is
+ * closed.
+ * @return the AdaptiveBulkheadConfig.Builder
+ * @throws IllegalArgumentException if {@code slidingWindowSize < 1}
+ */
+ public Builder slidingWindowSize(int slidingWindowSize) {
+ if (slidingWindowSize < 1) {
+ throw new IllegalArgumentException("slidingWindowSize must be greater than 0");
+ }
+ this.slidingWindowSize = slidingWindowSize;
+ return this;
+ }
+
+ /**
+ * Configures the type of the sliding window which is used to record the outcome of calls.
+ * Sliding window can either be count-based or time-based.
+ *
+ * Default slidingWindowType is COUNT_BASED.
+ *
+ * @param slidingWindowType the type of the sliding window. Either COUNT_BASED or
+ * TIME_BASED.
+ * @return the AdaptiveBulkheadConfig.Builder
+ */
+ public Builder slidingWindowType(SlidingWindowType slidingWindowType) {
+ this.slidingWindowType = slidingWindowType;
+ return this;
+ }
+
+ /**
+ * Configures the failure rate threshold in percentage.
+ *
+ * The threshold must be greater than 0 and not greater than 100. Default value is 50
+ * percentage.
+ *
+ * @param failureRateThreshold the failure rate threshold in percentage
+ * @return the AdaptiveBulkheadConfig.Builder
+ * @throws IllegalArgumentException if {@code failureRateThreshold <= 0 ||
+ * failureRateThreshold > 100}
+ */
+ public Builder failureRateThreshold(float failureRateThreshold) {
+ if (failureRateThreshold <= 0 || failureRateThreshold > 100) {
+ throw new IllegalArgumentException(
+ "failureRateThreshold must be between 1 and 100");
+ }
+ this.failureRateThreshold = failureRateThreshold;
+ return this;
+ }
+
+ /**
+ * Configures a Predicate which evaluates if an exception should be ignored and neither count as a failure nor success.
+ * The Predicate must return true if the exception must be ignored .
+ * The Predicate must return false, if the exception must count as a failure.
+ *
+ * @param predicate the Predicate which checks if an exception should count as a failure
+ * @return the Builder
+ */
+ public final Builder ignoreException(Predicate predicate) {
+ this.ignoreExceptionPredicate = predicate;
+ return this;
+ }
+
+ /**
+ * Configures a list of error classes that are recorded as a failure and thus increase the failure rate.
+ * an exception matching or inheriting from one of the list should count as a failure
+ *
+ * @param errorClasses the error classes which are recorded
+ * @return the Builder
+ * @see #ignoreExceptions(Class[]) ). Ignoring an exception has more priority over recording an exception.
+ */
+ @SuppressWarnings("unchecked")
+ @SafeVarargs
+ public final Builder recordExceptions(@Nullable Class extends Throwable>... errorClasses) {
+ this.recordExceptions = errorClasses != null ? errorClasses : new Class[0];
+ return this;
+ }
+
+ /**
+ * Configures a Predicate which evaluates if an exception should be recorded as a failure and thus increase the failure rate.
+ * The Predicate must return true if the exception should count as a failure. The Predicate must return false, if the exception should count as a success
+ * ,unless the exception is explicitly ignored by {@link #ignoreExceptions(Class[])} or {@link #ignoreException(Predicate)}.
+ *
+ * @param predicate the Predicate which checks if an exception should count as a failure
+ * @return the Builder
+ */
+ public final Builder recordException(Predicate predicate) {
+ this.recordExceptionPredicate = predicate;
+ return this;
+ }
+
+ /**
+ * Configures a list of error classes that are ignored and thus neither count as a failure nor success.
+ * an exception matching or inheriting from one of that list will not count as a failure nor success
+ *
+ * @param errorClasses the error classes which are ignored
+ * @return the Builder
+ * @see #recordExceptions(Class[]) . Ignoring an exception has priority over recording an exception.
+ */
+ @SuppressWarnings("unchecked")
+ @SafeVarargs
+ public final Builder ignoreExceptions(@Nullable Class extends Throwable>... errorClasses) {
+ this.ignoreExceptions = errorClasses != null ? errorClasses : new Class[0];
+ return this;
+ }
+
+ /**
+ * Configures the minimum number of calls which are required (per sliding window period)
+ * before the AdaptiveBulkhead can calculate the error or slow rate. For example, if {@code
+ * minimumNumberOfCalls} is 10, then at least 10 calls must be recorded, before the failure
+ * rate can be calculated.
+ *
+ * Default minimumNumberOfCalls is 100
+ *
+ * @param minimumNumberOfCalls the minimum number of calls that must be recorded before the
+ * failure rate can be calculated.
+ * @return the Builder
+ * @throws IllegalArgumentException if {@code minimumNumberOfCalls < 1}
+ */
+ public Builder minimumNumberOfCalls(int minimumNumberOfCalls) {
+ if (minimumNumberOfCalls < 1) {
+ throw new IllegalArgumentException("minimumNumberOfCalls must be greater than 0");
+ }
+ this.minimumNumberOfCalls = minimumNumberOfCalls;
+ return this;
+ }
+
+ public final Builder writableStackTraceEnabled(boolean writableStackTraceEnabled) {
+ this.writableStackTraceEnabled = writableStackTraceEnabled;
+ return this;
+ }
+
+ public final Builder minConcurrentCalls(int minConcurrentCalls) {
+ if (minConcurrentCalls <= 0) {
+ throw new IllegalArgumentException(
+ "minConcurrentCalls must greater than 0");
+ }
+ this.minConcurrentCalls = minConcurrentCalls;
+ return this;
+ }
+
+ public final Builder maxConcurrentCalls(int maxConcurrentCalls) {
+ if (maxConcurrentCalls <= 0) {
+ throw new IllegalArgumentException(
+ "maxConcurrentCalls must greater than 0");
+ }
+ this.maxConcurrentCalls = maxConcurrentCalls;
+ return this;
+ }
+
+ public final Builder initialConcurrentCalls(int initialConcurrentCalls) {
+ if (initialConcurrentCalls <= 0) {
+ throw new IllegalArgumentException(
+ "initialConcurrentCalls must greater than 0");
+ }
+ this.initialConcurrentCalls = initialConcurrentCalls;
+ return this;
+ }
+
+ public final Builder increaseSummand(int increaseSummand) {
+ if (increaseSummand <= 0) {
+ throw new IllegalArgumentException(
+ "increaseSummand must greater than 0");
+ }
+ this.increaseSummand = increaseSummand;
+ return this;
+ }
+
+ public final Builder decreaseMultiplier(float decreaseMultiplier) {
+ if (decreaseMultiplier < 0 || decreaseMultiplier > 1) {
+ throw new IllegalArgumentException(
+ "decreaseMultiplier must be between 0 and 1");
+ }
+ this.decreaseMultiplier = decreaseMultiplier;
+ return this;
+ }
+
+ /**
+ * Configures a maximum amount of time which the calling thread will wait to enter the
+ * bulkhead. If bulkhead has space available, entry is guaranteed and immediate. If bulkhead
+ * is full, calling threads will contest for space, if it becomes available. maxWaitDuration
+ * can be set to 0.
+ *
+ * Note: for threads running on an event-loop or equivalent (rx computation pool, etc),
+ * setting maxWaitDuration to 0 is highly recommended. Blocking an event-loop thread will
+ * most likely have a negative effect on application throughput.
+ *
+ * @param maxWaitDuration maximum wait time for bulkhead entry
+ * @return the BulkheadConfig.Builder
+ */
+ public AdaptiveBulkheadConfig.Builder maxWaitDuration(Duration maxWaitDuration) {
+ if (maxWaitDuration.toMillis() < 0) {
+ throw new IllegalArgumentException(
+ "maxWaitDuration must be a positive integer value >= 0");
+ }
+ this.maxWaitDuration = maxWaitDuration;
+ return this;
+ }
+
+ public AdaptiveBulkheadConfig build() {
+ AdaptiveBulkheadConfig config = new AdaptiveBulkheadConfig();
+ config.slidingWindowType = slidingWindowType;
+ config.slowCallDurationThreshold = slowCallDurationThreshold;
+ config.slowCallRateThreshold = slowCallRateThreshold;
+ config.failureRateThreshold = failureRateThreshold;
+ config.slidingWindowSize = slidingWindowSize;
+ config.minimumNumberOfCalls = minimumNumberOfCalls;
+ config.recordExceptions = recordExceptions;
+ config.ignoreExceptions = ignoreExceptions;
+ config.writableStackTraceEnabled = writableStackTraceEnabled;
+ config.minConcurrentCalls = minConcurrentCalls;
+ config.maxConcurrentCalls = maxConcurrentCalls;
+ config.initialConcurrentCalls = initialConcurrentCalls;
+ config.increaseSummand = increaseSummand;
+ config.decreaseMultiplier = decreaseMultiplier;
+ config.maxWaitDuration = maxWaitDuration;
+ config.recordExceptionPredicate = createRecordExceptionPredicate();
+ config.ignoreExceptionPredicate = createIgnoreFailurePredicate();
+ return config;
+ }
+
+ private Predicate createIgnoreFailurePredicate() {
+ return PredicateCreator.createExceptionsPredicate(ignoreExceptions)
+ .map(predicate -> ignoreExceptionPredicate != null ? predicate
+ .or(ignoreExceptionPredicate) : predicate)
+ .orElseGet(() -> ignoreExceptionPredicate != null ? ignoreExceptionPredicate
+ : DEFAULT_IGNORE_EXCEPTION_PREDICATE);
+ }
+
+ private Predicate createRecordExceptionPredicate() {
+ return PredicateCreator.createExceptionsPredicate(recordExceptions)
+ .map(predicate -> recordExceptionPredicate != null ? predicate
+ .or(recordExceptionPredicate) : predicate)
+ .orElseGet(() -> recordExceptionPredicate != null ? recordExceptionPredicate
+ : DEFAULT_RECORD_EXCEPTION_PREDICATE);
+ }
+
+ }
+
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadRegistry.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadRegistry.java
new file mode 100644
index 0000000000..721112508d
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadRegistry.java
@@ -0,0 +1,105 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.adaptive;
+
+
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import io.github.resilience4j.bulkhead.adaptive.internal.InMemoryAdaptiveBulkheadRegistry;
+import io.github.resilience4j.core.Registry;
+
+/**
+ * The {@link AdaptiveBulkheadRegistry} is a factory to create AdaptiveBulkhead instances which stores all bulkhead instances in a registry.
+ */
+public interface AdaptiveBulkheadRegistry extends Registry {
+
+ /**
+ * Returns all managed {@link AdaptiveBulkhead} instances.
+ *
+ * @return all managed {@link AdaptiveBulkhead} instances.
+ */
+ Set getAllBulkheads();
+
+ /**
+ * Returns a managed {@link AdaptiveBulkhead} or creates a new one with default configuration.
+ *
+ * @param name the name of the AdaptiveBulkhead
+ * @return The {@link AdaptiveBulkhead}
+ */
+ AdaptiveBulkhead bulkhead(String name);
+
+ /**
+ * Returns a managed {@link AdaptiveBulkhead} or creates a new one with a custom AdaptiveBulkheadConfig configuration.
+ *
+ * @param name the name of the AdaptiveBulkhead
+ * @param config a custom AdaptiveBulkhead configuration
+ * @return The {@link AdaptiveBulkhead}
+ */
+ AdaptiveBulkhead bulkhead(String name, AdaptiveBulkheadConfig config);
+
+ /**
+ * Returns a managed {@link AdaptiveBulkhead} or creates a new one with a custom AdaptiveBulkhead configuration.
+ *
+ * @param name the name of the AdaptiveBulkhead
+ * @param bulkheadConfigSupplier a custom AdaptiveBulkhead configuration supplier
+ * @return The {@link AdaptiveBulkhead}
+ */
+ AdaptiveBulkhead bulkhead(String name, Supplier bulkheadConfigSupplier);
+
+ /**
+ * Returns a managed {@link AdaptiveBulkhead} or creates a new one with a custom AdaptiveBulkhead configuration.
+ *
+ * @param name the name of the AdaptiveBulkhead
+ * @param configName a custom AdaptiveBulkhead configuration name
+ * @return The {@link AdaptiveBulkhead}
+ */
+ AdaptiveBulkhead bulkhead(String name, String configName);
+
+ /**
+ * Creates a BulkheadRegistry with a custom AdaptiveBulkhead configuration.
+ *
+ * @param bulkheadConfig a custom AdaptiveBulkhead configuration
+ * @return a BulkheadRegistry instance backed by a custom AdaptiveBulkhead configuration
+ */
+ static AdaptiveBulkheadRegistry of(AdaptiveBulkheadConfig bulkheadConfig) {
+ return new InMemoryAdaptiveBulkheadRegistry(bulkheadConfig);
+ }
+
+ /**
+ * Creates a BulkheadRegistry with a Map of shared Bulkhead configurations.
+ *
+ * @param configs a Map of shared AdaptiveBulkhead configurations
+ * @return a RetryRegistry with a Map of shared AdaptiveBulkhead configurations.
+ */
+ static AdaptiveBulkheadRegistry of(Map configs) {
+ return new InMemoryAdaptiveBulkheadRegistry(configs);
+ }
+
+ /**
+ * Creates a BulkheadRegistry with a default AdaptiveBulkhead configuration
+ *
+ * @return a BulkheadRegistry instance backed by a default Bulkhead configuration
+ */
+ static AdaptiveBulkheadRegistry ofDefaults() {
+ return new InMemoryAdaptiveBulkheadRegistry(AdaptiveBulkheadConfig.ofDefaults());
+ }
+
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptationCalculator.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptationCalculator.java
new file mode 100644
index 0000000000..5a50f1c3a7
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptationCalculator.java
@@ -0,0 +1,74 @@
+/*
+ *
+ * Copyright 2021: Tomasz SkowroĊski
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.adaptive.internal;
+
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkheadConfig;
+
+class AdaptationCalculator {
+
+ private final AdaptiveBulkhead adaptiveBulkhead;
+
+ AdaptationCalculator(AdaptiveBulkhead adaptiveBulkhead) {
+ this.adaptiveBulkhead = adaptiveBulkhead;
+ }
+
+ /**
+ * additive increase
+ *
+ * @return new concurrency limit to apply
+ */
+ int increment() {
+ return fitToRange(
+ getActualConcurrencyLimit() + getConfig().getIncreaseSummand());
+ }
+
+ /**
+ * multiplicative increase
+ *
+ * @return new concurrency limit to apply
+ */
+ int increase() {
+ return fitToRange(
+ (int) (getActualConcurrencyLimit() * getConfig().getIncreaseMultiplier()));
+ }
+
+ /**
+ * multiplicative decrease
+ *
+ * @return new concurrency limit to apply
+ */
+ int decrease() {
+ return fitToRange(
+ (int) (getActualConcurrencyLimit() * getConfig().getDecreaseMultiplier()));
+ }
+
+ private AdaptiveBulkheadConfig getConfig() {
+ return adaptiveBulkhead.getBulkheadConfig();
+ }
+
+ private int getActualConcurrencyLimit() {
+ return adaptiveBulkhead.getMetrics().getMaxAllowedConcurrentCalls();
+ }
+
+ private int fitToRange(int concurrencyLimitProposal) {
+ return Math.min(getConfig().getMaxConcurrentCalls(),
+ Math.max(getConfig().getMinConcurrentCalls(), concurrencyLimitProposal));
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadEventProcessor.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadEventProcessor.java
new file mode 100644
index 0000000000..d9158d2b5a
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadEventProcessor.java
@@ -0,0 +1,56 @@
+package io.github.resilience4j.bulkhead.adaptive.internal;
+
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+import io.github.resilience4j.bulkhead.event.*;
+import io.github.resilience4j.core.EventConsumer;
+import io.github.resilience4j.core.EventProcessor;
+import io.github.resilience4j.core.EventPublisher;
+
+class AdaptiveBulkheadEventProcessor extends EventProcessor implements
+ AdaptiveBulkhead.AdaptiveEventPublisher, EventConsumer {
+
+ @Override
+ public EventPublisher> onLimitIncreased(
+ EventConsumer eventConsumer) {
+ registerConsumer(BulkheadOnLimitIncreasedEvent.class.getName(), eventConsumer);
+ return this;
+ }
+
+ @Override
+ public EventPublisher> onLimitDecreased(
+ EventConsumer eventConsumer) {
+ registerConsumer(BulkheadOnLimitDecreasedEvent.class.getName(), eventConsumer);
+ return this;
+ }
+
+ @Override
+ public EventPublisher> onSuccess(EventConsumer eventConsumer) {
+ registerConsumer(BulkheadOnSuccessEvent.class.getName(), eventConsumer);
+ return this;
+ }
+
+ @Override
+ public EventPublisher> onError(EventConsumer eventConsumer) {
+ registerConsumer(BulkheadOnErrorEvent.class.getName(), eventConsumer);
+ return this;
+ }
+
+ @Override
+ public EventPublisher> onIgnoredError(
+ EventConsumer eventConsumer) {
+ registerConsumer(BulkheadOnIgnoreEvent.class.getName(), eventConsumer);
+ return this;
+ }
+
+ @Override
+ public EventPublisher> onStateTransition(
+ EventConsumer eventConsumer) {
+ registerConsumer(BulkheadOnStateTransitionEvent.class.getName(), eventConsumer);
+ return this;
+ }
+
+ @Override
+ public void consumeEvent(AdaptiveBulkheadEvent event) {
+ super.processEvent(event);
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadMetrics.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadMetrics.java
new file mode 100644
index 0000000000..7dabd18931
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadMetrics.java
@@ -0,0 +1,214 @@
+/*
+ *
+ * Copyright 2016 Robert Winkler and Bohdan Storozhuk
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.adaptive.internal;
+
+
+import io.github.resilience4j.bulkhead.Bulkhead;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkheadConfig;
+import io.github.resilience4j.core.metrics.FixedSizeSlidingWindowMetrics;
+import io.github.resilience4j.core.metrics.Metrics;
+import io.github.resilience4j.core.metrics.SlidingTimeWindowMetrics;
+import io.github.resilience4j.core.metrics.Snapshot;
+
+import java.time.Clock;
+import java.util.concurrent.TimeUnit;
+
+import static io.github.resilience4j.core.metrics.Metrics.Outcome;
+
+class AdaptiveBulkheadMetrics implements AdaptiveBulkhead.Metrics {
+
+ private final Metrics slidingWindowMetrics;
+ private final float failureRateThreshold;
+ private final float slowCallRateThreshold;
+ private final long slowCallDurationThresholdInNanos;
+ private final int minimumNumberOfCalls;
+ private final Bulkhead.Metrics internalBulkheadMetrics;
+
+ private AdaptiveBulkheadMetrics(int slidingWindowSize,
+ AdaptiveBulkheadConfig.SlidingWindowType slidingWindowType,
+ AdaptiveBulkheadConfig adaptiveBulkheadConfig,
+ Bulkhead.Metrics internalBulkheadMetrics,
+ Clock clock) {
+ if (slidingWindowType == AdaptiveBulkheadConfig.SlidingWindowType.COUNT_BASED) {
+ this.slidingWindowMetrics = new FixedSizeSlidingWindowMetrics(slidingWindowSize);
+ this.minimumNumberOfCalls = Math
+ .min(adaptiveBulkheadConfig.getMinimumNumberOfCalls(), slidingWindowSize);
+ } else {
+ this.slidingWindowMetrics = new SlidingTimeWindowMetrics(slidingWindowSize, clock);
+ this.minimumNumberOfCalls = adaptiveBulkheadConfig.getMinimumNumberOfCalls();
+ }
+ this.failureRateThreshold = adaptiveBulkheadConfig.getFailureRateThreshold();
+ this.slowCallRateThreshold = adaptiveBulkheadConfig.getSlowCallRateThreshold();
+ this.slowCallDurationThresholdInNanos = adaptiveBulkheadConfig
+ .getSlowCallDurationThreshold().toNanos();
+ this.internalBulkheadMetrics = internalBulkheadMetrics;
+ }
+
+ public AdaptiveBulkheadMetrics(AdaptiveBulkheadConfig adaptiveBulkheadConfig,
+ Bulkhead.Metrics internalBulkheadMetrics,
+ Clock clock) {
+ this(adaptiveBulkheadConfig.getSlidingWindowSize(),
+ adaptiveBulkheadConfig.getSlidingWindowType(),
+ adaptiveBulkheadConfig,
+ internalBulkheadMetrics,
+ clock);
+ }
+
+ /**
+ * Records a successful call and checks if the thresholds are exceeded.
+ *
+ * @return the result of the check
+ */
+ public Result onSuccess(long duration, TimeUnit durationUnit) {
+ return checkIfThresholdsExceeded(record(duration, durationUnit, true));
+ }
+
+ /**
+ * Records a failed call and checks if the thresholds are exceeded.
+ *
+ * @return the result of the check
+ */
+ public Result onError(long duration, TimeUnit durationUnit) {
+ return checkIfThresholdsExceeded(record(duration, durationUnit, false));
+ }
+
+ Snapshot record(long duration, TimeUnit durationUnit, boolean success) {
+ boolean slow = durationUnit.toNanos(duration) > slowCallDurationThresholdInNanos;
+ return slidingWindowMetrics.record(duration, durationUnit, Outcome.of(slow, success));
+ }
+
+ /**
+ * Checks if the failure rate is above the threshold or if the slow calls percentage is above
+ * the threshold.
+ *
+ * @param snapshot a metrics snapshot
+ * @return false, if the thresholds haven't been exceeded.
+ */
+ private Result checkIfThresholdsExceeded(Snapshot snapshot) {
+ if (isBelowMinimumNumberOfCalls(snapshot)) {
+ return Result.UNRELIABLE_THRESHOLDS;
+ } else if (snapshot.getFailureRate() >= failureRateThreshold
+ || snapshot.getSlowCallRate() >= slowCallRateThreshold) {
+ return Result.ABOVE_THRESHOLDS;
+ } else {
+ return Result.BELOW_THRESHOLDS;
+ }
+ }
+
+ private boolean isBelowMinimumNumberOfCalls(Snapshot snapshot) {
+ return snapshot.getTotalNumberOfCalls() == 0
+ || snapshot.getTotalNumberOfCalls() < minimumNumberOfCalls;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float getFailureRate() {
+ Snapshot snapshot = slidingWindowMetrics.getSnapshot();
+ return isBelowMinimumNumberOfCalls(snapshot) ? -1 : snapshot.getFailureRate();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float getSlowCallRate() {
+ Snapshot snapshot = slidingWindowMetrics.getSnapshot();
+ return isBelowMinimumNumberOfCalls(snapshot) ? -1 : snapshot.getSlowCallRate();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getNumberOfSuccessfulCalls() {
+ return slidingWindowMetrics.getSnapshot().getNumberOfSuccessfulCalls();
+ }
+
+ @Override
+ public void resetRecords() {
+ slidingWindowMetrics.resetRecords();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getNumberOfBufferedCalls() {
+ return slidingWindowMetrics.getSnapshot().getTotalNumberOfCalls();
+ }
+
+ @Override
+ public int getNumberOfFailedCalls() {
+ return slidingWindowMetrics.getSnapshot().getNumberOfFailedCalls();
+ }
+
+ @Override
+ public int getNumberOfSlowCalls() {
+ return slidingWindowMetrics.getSnapshot().getTotalNumberOfSlowCalls();
+ }
+
+ @Override
+ public int getNumberOfSlowSuccessfulCalls() {
+ return slidingWindowMetrics.getSnapshot().getNumberOfSlowSuccessfulCalls();
+ }
+
+ @Override
+ public int getNumberOfSlowFailedCalls() {
+ return slidingWindowMetrics.getSnapshot().getNumberOfSlowFailedCalls();
+ }
+
+ Snapshot getSnapshot() {
+ return slidingWindowMetrics.getSnapshot();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getAvailableConcurrentCalls() {
+ return internalBulkheadMetrics.getAvailableConcurrentCalls();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getMaxAllowedConcurrentCalls() {
+ return internalBulkheadMetrics.getMaxAllowedConcurrentCalls();
+ }
+
+ enum Result {
+ /**
+ * Is below the error or slow calls rate.
+ */
+ BELOW_THRESHOLDS,
+ /**
+ * Is above the error or slow calls rate.
+ */
+ ABOVE_THRESHOLDS,
+ /**
+ * Is below minimum number of calls which are required (per sliding window period) before
+ * the Adaptive Bulkhead can calculate the reliable error or slow calls rate.
+ */
+ UNRELIABLE_THRESHOLDS
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadState.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadState.java
new file mode 100644
index 0000000000..f28f24a373
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadState.java
@@ -0,0 +1,33 @@
+package io.github.resilience4j.bulkhead.adaptive.internal;
+
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+import io.github.resilience4j.bulkhead.event.AdaptiveBulkheadEvent;
+
+import java.util.concurrent.TimeUnit;
+
+interface AdaptiveBulkheadState {
+
+ boolean tryAcquirePermission();
+
+ void acquirePermission();
+
+ void releasePermission();
+
+ void onError(long duration, TimeUnit durationUnit, Throwable throwable);
+
+ void onSuccess(long duration, TimeUnit durationUnit);
+
+ AdaptiveBulkhead.State getState();
+
+ AdaptiveBulkheadMetrics getMetrics();
+
+ /**
+ * Should the AdaptiveBulkhead in this state publish events
+ *
+ * @return a boolean signaling if the events should be published
+ */
+ default boolean shouldPublishEvents(AdaptiveBulkheadEvent event) {
+ return event.getEventType().forcePublish || getState().allowPublish;
+ }
+
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadStateMachine.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadStateMachine.java
new file mode 100644
index 0000000000..b838e8d2ca
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadStateMachine.java
@@ -0,0 +1,427 @@
+/*
+ *
+ * Copyright 2019: Bohdan Storozhuk, Mahmoud Romeh, Tomasz SkowroĊski
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.adaptive.internal;
+
+import io.github.resilience4j.bulkhead.Bulkhead;
+import io.github.resilience4j.bulkhead.BulkheadConfig;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkheadConfig;
+import io.github.resilience4j.bulkhead.event.*;
+import io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead;
+import io.github.resilience4j.core.lang.NonNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.UnaryOperator;
+
+public class AdaptiveBulkheadStateMachine implements AdaptiveBulkhead {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AdaptiveBulkheadStateMachine.class);
+ // TODO remove
+ public static final boolean RESET_METRICS_ON_TRANSITION = true;
+
+ private final String name;
+ private final AtomicReference stateReference;
+ private final AdaptiveBulkheadConfig adaptiveBulkheadConfig;
+ private final AdaptiveBulkheadEventProcessor eventProcessor;
+ private final Clock clock;
+ private final AdaptiveBulkheadMetrics metrics;
+ private final Bulkhead innerBulkhead;
+ private final AdaptationCalculator adaptationCalculator;
+
+ public AdaptiveBulkheadStateMachine(@NonNull String name,
+ @NonNull AdaptiveBulkheadConfig adaptiveBulkheadConfig) {
+ this.name = name;
+ this.adaptiveBulkheadConfig = Objects
+ .requireNonNull(adaptiveBulkheadConfig, "Config must not be null");
+ BulkheadConfig internalBulkheadConfig = BulkheadConfig.custom()
+ .maxConcurrentCalls(adaptiveBulkheadConfig.getInitialConcurrentCalls())
+ .maxWaitDuration(adaptiveBulkheadConfig.getMaxWaitDuration())
+ .build();
+ this.innerBulkhead = new SemaphoreBulkhead(name + "-internal", internalBulkheadConfig);
+ this.clock = Clock.systemUTC();
+ this.metrics = new AdaptiveBulkheadMetrics(adaptiveBulkheadConfig, innerBulkhead.getMetrics(), clock);
+ this.stateReference = new AtomicReference<>(new SlowStartState(metrics));
+ this.eventProcessor = new AdaptiveBulkheadEventProcessor();
+ this.adaptationCalculator = new AdaptationCalculator(this);
+ }
+
+ @Override
+ public boolean tryAcquirePermission() {
+ return stateReference.get().tryAcquirePermission();
+ }
+
+ @Override
+ public void acquirePermission() {
+ stateReference.get().acquirePermission();
+ }
+
+ @Override
+ public void releasePermission() {
+ stateReference.get().releasePermission();
+ }
+
+ /**
+ * @param duration call time
+ * @param durationUnit call time unit
+ */
+ @Override
+ public void onSuccess(long duration, TimeUnit durationUnit) {
+ releasePermission(); // ?
+ stateReference.get().onSuccess(duration, durationUnit);
+ publishBulkheadEvent(new BulkheadOnSuccessEvent(
+ shortName(innerBulkhead), Collections.emptyMap()));
+ }
+
+ /**
+ * @param startTime call start time in millis or 0
+ * @param durationUnit call time unit
+ * @param throwable an error
+ */
+ @Override
+ public void onError(long startTime, TimeUnit durationUnit, Throwable throwable) {
+ if (adaptiveBulkheadConfig.getIgnoreExceptionPredicate().test(throwable)) {
+ releasePermission();
+ publishBulkheadEvent(new BulkheadOnIgnoreEvent(
+ shortName(innerBulkhead), errorData(throwable)));
+ } else if (startTime != 0
+ && adaptiveBulkheadConfig.getRecordExceptionPredicate().test(throwable)) {
+ releasePermission(); // ?
+ stateReference.get().onError(timeUntilNow(startTime), durationUnit, throwable);
+ publishBulkheadEvent(new BulkheadOnErrorEvent(
+ shortName(innerBulkhead), errorData(throwable)));
+ } else if (startTime != 0) {
+ onSuccess(timeUntilNow(startTime), durationUnit);
+ }
+ }
+
+ private long timeUntilNow(long start) {
+ return Duration.between(Instant.ofEpochMilli(start), Instant.now()).toMillis();
+ }
+
+ @Override
+ public AdaptiveBulkheadConfig getBulkheadConfig() {
+ return adaptiveBulkheadConfig;
+ }
+
+ @Override
+ public Metrics getMetrics() {
+ return metrics;
+ }
+
+ @Override
+ public AdaptiveEventPublisher getEventPublisher() {
+ return eventProcessor;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void transitionToCongestionAvoidance() {
+ stateTransition(State.CONGESTION_AVOIDANCE,
+ current -> new CongestionAvoidance(current.getMetrics()));
+ }
+
+ @Override
+ public void transitionToSlowStart() {
+ stateTransition(State.SLOW_START,
+ current -> new SlowStartState(current.getMetrics()));
+ }
+
+ private void stateTransition(State newState,
+ UnaryOperator newStateGenerator) {
+ LOG.debug("stateTransition to {}", newState);
+ AdaptiveBulkheadState previous = stateReference.getAndUpdate(newStateGenerator);
+ publishBulkheadEvent(new BulkheadOnStateTransitionEvent(
+ name, Collections.emptyMap(), previous.getState(), newState));
+ }
+
+ private void changeConcurrencyLimit(int newValue) {
+ int oldValue = innerBulkhead.getBulkheadConfig().getMaxConcurrentCalls();
+ if (newValue > oldValue) {
+ changeInternals(oldValue, newValue);
+ publishBulkheadOnLimitIncreasedEvent(newValue);
+ } else if (newValue < oldValue) {
+ changeInternals(oldValue, newValue);
+ publishBulkheadOnLimitDecreasedEvent(newValue);
+ }
+ }
+
+ private void changeInternals(int oldValue, int newValue) {
+ LOG.debug("changeConcurrencyLimit from {} to {}", oldValue, newValue);
+ innerBulkhead.changeConfig(
+ BulkheadConfig.from(innerBulkhead.getBulkheadConfig())
+ .maxConcurrentCalls(newValue)
+ .build());
+ if (RESET_METRICS_ON_TRANSITION) {
+ metrics.resetRecords();
+ }
+ }
+
+ private void publishBulkheadOnLimitIncreasedEvent(int maxConcurrentCalls) {
+ publishBulkheadEvent(new BulkheadOnLimitIncreasedEvent(
+ shortName(innerBulkhead),
+ limitChangeEventData(
+ innerBulkhead.getBulkheadConfig().getMaxWaitDuration().toMillis(),
+ maxConcurrentCalls)));
+ }
+
+ private void publishBulkheadOnLimitDecreasedEvent(int maxConcurrentCalls) {
+ publishBulkheadEvent(new BulkheadOnLimitDecreasedEvent(
+ shortName(innerBulkhead),
+ limitChangeEventData(
+ innerBulkhead.getBulkheadConfig().getMaxWaitDuration().toMillis(),
+ maxConcurrentCalls)));
+ }
+
+ /**
+ * Although the strategy is referred to as slow start, its congestion window growth is quite
+ * aggressive, more aggressive than the congestion avoidance phase.
+ */
+ private class SlowStartState implements AdaptiveBulkheadState {
+
+ private final AdaptiveBulkheadMetrics adaptiveBulkheadMetrics;
+ private final AtomicBoolean slowStart;
+
+ SlowStartState(AdaptiveBulkheadMetrics adaptiveBulkheadMetrics) {
+ this.adaptiveBulkheadMetrics = adaptiveBulkheadMetrics;
+ this.slowStart = new AtomicBoolean(true);
+ }
+
+ @Override
+ public boolean tryAcquirePermission() {
+ return slowStart.get() && innerBulkhead.tryAcquirePermission();
+ }
+
+ @Override
+ public void acquirePermission() {
+ innerBulkhead.acquirePermission();
+ }
+
+ @Override
+ public void releasePermission() {
+ innerBulkhead.releasePermission();
+ }
+
+ @Override
+ public void onError(long duration, TimeUnit durationUnit, Throwable throwable) {
+ // AdaptiveBulkheadMetrics is thread-safe
+ checkIfThresholdsExceeded(adaptiveBulkheadMetrics.onError(duration, durationUnit));
+ }
+
+ @Override
+ public void onSuccess(long duration, TimeUnit durationUnit) {
+ // AdaptiveBulkheadMetrics is thread-safe
+ checkIfThresholdsExceeded(adaptiveBulkheadMetrics.onSuccess(duration, durationUnit));
+ }
+
+ /**
+ * Transitions to CONGESTION_AVOIDANCE state when thresholds have been exceeded.
+ *
+ * @param result the Result
+ */
+ private void checkIfThresholdsExceeded(AdaptiveBulkheadMetrics.Result result) {
+ logStateDetails(result);
+ if (slowStart.get()) {
+ switch (result) {
+ case BELOW_THRESHOLDS:
+ changeConcurrencyLimit(adaptationCalculator.increase());
+ break;
+ case ABOVE_THRESHOLDS:
+ if (slowStart.compareAndSet(true, false)) {
+ changeConcurrencyLimit(adaptationCalculator.decrease());
+ transitionToCongestionAvoidance();
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Get the state of the AdaptiveBulkhead
+ */
+ @Override
+ public AdaptiveBulkhead.State getState() {
+ return State.SLOW_START;
+ }
+
+ /**
+ * Get metrics of the AdaptiveBulkhead
+ */
+ @Override
+ public AdaptiveBulkheadMetrics getMetrics() {
+ return adaptiveBulkheadMetrics;
+ }
+
+ }
+
+ @Deprecated
+ private void logStateDetails(AdaptiveBulkheadMetrics.Result result) {
+ LOG.debug("calls:{}/{}, rate:{} result:{}",
+ metrics.getNumberOfBufferedCalls(),
+ adaptiveBulkheadConfig.getMinimumNumberOfCalls(),
+ Math.max(metrics.getSnapshot().getFailureRate(), metrics.getSnapshot().getSlowCallRate()),
+ result);
+ }
+
+
+ private class CongestionAvoidance implements AdaptiveBulkheadState {
+
+ private final AdaptiveBulkheadMetrics adaptiveBulkheadMetrics;
+ private final AtomicBoolean congestionAvoidance;
+
+ CongestionAvoidance(AdaptiveBulkheadMetrics adaptiveBulkheadMetrics) {
+ this.adaptiveBulkheadMetrics = adaptiveBulkheadMetrics;
+ this.congestionAvoidance = new AtomicBoolean(true);
+ }
+
+ @Override
+ public boolean tryAcquirePermission() {
+ return congestionAvoidance.get() && innerBulkhead.tryAcquirePermission();
+ }
+
+ @Override
+ public void acquirePermission() {
+ innerBulkhead.acquirePermission();
+ }
+
+ @Override
+ public void releasePermission() {
+ innerBulkhead.releasePermission();
+ }
+
+ @Override
+ public void onError(long duration, TimeUnit durationUnit, Throwable throwable) {
+ // AdaptiveBulkheadMetrics is thread-safe
+ checkIfThresholdsExceeded(adaptiveBulkheadMetrics.onError(duration, durationUnit));
+ }
+
+ @Override
+ public void onSuccess(long duration, TimeUnit durationUnit) {
+ // AdaptiveBulkheadMetrics is thread-safe
+ checkIfThresholdsExceeded(adaptiveBulkheadMetrics.onSuccess(duration, durationUnit));
+ }
+
+ /**
+ * Transitions to SLOW_START state when Minimum Concurrency Limit have been reached.
+ *
+ * @param result the Result
+ */
+ private void checkIfThresholdsExceeded(AdaptiveBulkheadMetrics.Result result) {
+ logStateDetails(result);
+ if (congestionAvoidance.get()) {
+ switch (result) {
+ case BELOW_THRESHOLDS:
+ if (isConcurrencyLimitTooLow()) {
+ if (congestionAvoidance.compareAndSet(true, false)) {
+ transitionToSlowStart();
+ }
+ } else {
+ changeConcurrencyLimit(adaptationCalculator.increment());
+ }
+ break;
+ case ABOVE_THRESHOLDS:
+ changeConcurrencyLimit(adaptationCalculator.decrease());
+ break;
+ }
+ }
+ }
+
+ private boolean isConcurrencyLimitTooLow() {
+ return getMetrics().getMaxAllowedConcurrentCalls()
+ == adaptiveBulkheadConfig.getMinConcurrentCalls();
+ }
+
+ /**
+ * Get the state of the AdaptiveBulkhead
+ */
+ @Override
+ public AdaptiveBulkhead.State getState() {
+ return State.CONGESTION_AVOIDANCE;
+ }
+
+ /**
+ * Get metrics of the AdaptiveBulkhead
+ */
+ @Override
+ public AdaptiveBulkheadMetrics getMetrics() {
+ return adaptiveBulkheadMetrics;
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ return String.format("AdaptiveBulkhead '%s'", this.name);
+ }
+
+ /**
+ * @param eventSupplier the event supplier to be pushed to consumers
+ */
+ private void publishBulkheadEvent(AdaptiveBulkheadEvent eventSupplier) {
+ if (eventProcessor.hasConsumers()) {
+ eventProcessor.consumeEvent(eventSupplier);
+ }
+ }
+
+ // it's a workaround for "-internal" suffix
+ @Deprecated
+ private static String shortName(Bulkhead bulkhead) {
+ int cut = bulkhead.getName().indexOf('-');
+ return cut > 0 ? bulkhead.getName().substring(0, cut) : bulkhead.getName();
+ }
+
+ /**
+ * @param waitTimeMillis new wait time
+ * @param newMaxConcurrentCalls new max concurrent data
+ * @return map of kep value string of the event properties
+ */
+ private static Map limitChangeEventData(long waitTimeMillis,
+ int newMaxConcurrentCalls) {
+ Map eventData = new HashMap<>();
+ eventData.put("newMaxConcurrentCalls", String.valueOf(newMaxConcurrentCalls));
+ // TODO do we need newWaitTimeMillis here?
+ eventData.put("newWaitTimeMillis", String.valueOf(waitTimeMillis));
+ return eventData;
+ }
+
+ /**
+ * @param throwable error exception to be wrapped into the event data
+ * @return map of kep value string of the event properties
+ */
+ private Map errorData(Throwable throwable) {
+ Map eventData = new HashMap<>();
+ eventData.put("exceptionMsg", throwable.getMessage());
+ return eventData;
+ }
+
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/InMemoryAdaptiveBulkheadRegistry.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/InMemoryAdaptiveBulkheadRegistry.java
new file mode 100644
index 0000000000..0dbe00c6b4
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/InMemoryAdaptiveBulkheadRegistry.java
@@ -0,0 +1,101 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.adaptive.internal;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkheadConfig;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkheadRegistry;
+import io.github.resilience4j.core.ConfigurationNotFoundException;
+import io.github.resilience4j.core.registry.AbstractRegistry;
+
+/**
+ * Bulkhead instance manager;
+ * Constructs/returns AdaptiveBulkhead instances.
+ */
+public final class InMemoryAdaptiveBulkheadRegistry extends AbstractRegistry implements AdaptiveBulkheadRegistry {
+
+ /**
+ * The constructor with default default.
+ */
+ public InMemoryAdaptiveBulkheadRegistry() {
+ this(AdaptiveBulkheadConfig.ofDefaults());
+ }
+
+ public InMemoryAdaptiveBulkheadRegistry(Map configs) {
+ this(configs.getOrDefault(DEFAULT_CONFIG, AdaptiveBulkheadConfig.ofDefaults()));
+ this.configurations.putAll(configs);
+ }
+
+ /**
+ * The constructor with custom default config.
+ *
+ * @param defaultConfig The default config.
+ */
+ public InMemoryAdaptiveBulkheadRegistry(AdaptiveBulkheadConfig defaultConfig) {
+ super(defaultConfig);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Set getAllBulkheads() {
+ return new HashSet<>(entryMap.values());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AdaptiveBulkhead bulkhead(String name) {
+ return bulkhead(name, getDefaultConfig());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AdaptiveBulkhead bulkhead(String name, AdaptiveBulkheadConfig config) {
+ return computeIfAbsent(name, () -> AdaptiveBulkhead.of(name, Objects.requireNonNull(config, CONFIG_MUST_NOT_BE_NULL)));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AdaptiveBulkhead bulkhead(String name, Supplier bulkheadConfigSupplier) {
+ return computeIfAbsent(name, () -> AdaptiveBulkhead.of(name, Objects.requireNonNull(Objects.requireNonNull(bulkheadConfigSupplier, SUPPLIER_MUST_NOT_BE_NULL).get(), CONFIG_MUST_NOT_BE_NULL)));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AdaptiveBulkhead bulkhead(String name, String configName) {
+ return computeIfAbsent(name, () -> AdaptiveBulkhead.of(name, getConfiguration(configName)
+ .orElseThrow(() -> new ConfigurationNotFoundException(configName))));
+ }
+
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/package-info.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/package-info.java
new file mode 100644
index 0000000000..ff03dc1706
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/internal/package-info.java
@@ -0,0 +1,24 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.
+ *
+ *
+ */
+@NonNullApi
+@NonNullFields
+package io.github.resilience4j.bulkhead.adaptive.internal;
+
+import io.github.resilience4j.core.lang.NonNullApi;
+import io.github.resilience4j.core.lang.NonNullFields;
\ No newline at end of file
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/package-info.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/package-info.java
new file mode 100644
index 0000000000..eb1934fc8a
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/adaptive/package-info.java
@@ -0,0 +1,24 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.
+ *
+ *
+ */
+@NonNullApi
+@NonNullFields
+package io.github.resilience4j.bulkhead.adaptive;
+
+import io.github.resilience4j.core.lang.NonNullApi;
+import io.github.resilience4j.core.lang.NonNullFields;
\ No newline at end of file
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/AbstractBulkheadLimitEvent.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/AbstractBulkheadLimitEvent.java
new file mode 100644
index 0000000000..ae2f218b5c
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/AbstractBulkheadLimitEvent.java
@@ -0,0 +1,51 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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 speci`fic language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+package io.github.resilience4j.bulkhead.event;
+
+import java.time.ZonedDateTime;
+import java.util.Map;
+
+public abstract class AbstractBulkheadLimitEvent implements AdaptiveBulkheadEvent {
+
+ private final String bulkheadName;
+ // TODO replace by fields
+ private final Map eventData;
+ private final ZonedDateTime creationTime;
+
+ AbstractBulkheadLimitEvent(String bulkheadName, Map eventData) {
+ this.bulkheadName = bulkheadName;
+ this.eventData = eventData;
+ this.creationTime = ZonedDateTime.now();
+ }
+
+ @Override
+ public String getBulkheadName() {
+ return bulkheadName;
+ }
+
+ @Override
+ public Map eventData() {
+ return eventData;
+ }
+
+ @Override
+ public ZonedDateTime getCreationTime() {
+ return creationTime;
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/AdaptiveBulkheadEvent.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/AdaptiveBulkheadEvent.java
new file mode 100644
index 0000000000..9e9a7532e1
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/AdaptiveBulkheadEvent.java
@@ -0,0 +1,78 @@
+package io.github.resilience4j.bulkhead.event;
+
+import java.time.ZonedDateTime;
+import java.util.Map;
+
+/**
+ * adaptive bulkhead event
+ */
+public interface AdaptiveBulkheadEvent {
+ /**
+ * Returns the name of the bulkhead which has created the event.
+ *
+ * @return the name of the bulkhead which has created the event
+ */
+ String getBulkheadName();
+
+ /**
+ * Returns the type of the bulkhead event.
+ *
+ * @return the type of the bulkhead event
+ */
+ Type getEventType();
+
+
+ /**
+ * @return event related data like new max limit ..ect
+ */
+ Map eventData();
+
+
+ /**
+ * Returns the creation time of adaptive bulkhead event.
+ *
+ * @return the creation time of adaptive bulkhead event
+ */
+ ZonedDateTime getCreationTime();
+
+
+ /**
+ * Event types which are created by a bulkhead.
+ */
+ enum Type {
+ /**
+ * A AdaptiveBulkheadEvent which informs that a limit has been increased
+ */
+ LIMIT_INCREASED(false),
+ /**
+ * A AdaptiveBulkheadEvent which informs that a limit has been decreased
+ */
+ LIMIT_DECREASED(false),
+ /** An adaptive bulkhead event which informs that an error has been recorded */
+ ERROR(false),
+ /** An adaptive bulkhead which informs that an error has been ignored */
+ IGNORED_ERROR(false),
+ /** An adaptive bulkhead which informs that a success has been recorded */
+ SUCCESS(false),
+
+ /**
+ * A AdaptiveBulkheadEvent which informs the state of the AdaptiveBulkhead has been changed
+ */
+ STATE_TRANSITION(true),
+ /**
+ * A AdaptiveBulkheadEvent which informs the AdaptiveBulkhead has been reset
+ */
+ RESET(true),
+ /**
+ * A AdaptiveBulkheadEvent which informs the AdaptiveBulkhead has been disabled
+ */
+ DISABLED(false);
+
+ public final boolean forcePublish;
+
+ Type(boolean forcePublish) {
+ this.forcePublish = forcePublish;
+ }
+ }
+
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnErrorEvent.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnErrorEvent.java
new file mode 100644
index 0000000000..ceee19368b
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnErrorEvent.java
@@ -0,0 +1,45 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.event;
+
+import java.util.Map;
+
+/**
+ * A BulkheadEvent which informs that a call has been failed
+ */
+public class BulkheadOnErrorEvent extends AbstractBulkheadLimitEvent {
+
+ public BulkheadOnErrorEvent(String bulkheadName, Map eventData) {
+ super(bulkheadName, eventData);
+ }
+
+ @Override
+ public Type getEventType() {
+ return Type.ERROR;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s: Bulkhead '%s' call is failed.",
+ eventData(),
+ getBulkheadName()
+ );
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnIgnoreEvent.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnIgnoreEvent.java
new file mode 100644
index 0000000000..9ca5955dce
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnIgnoreEvent.java
@@ -0,0 +1,45 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.event;
+
+import java.util.Map;
+
+/**
+ * A BulkheadEvent which informs that an error has been ignored
+ */
+public class BulkheadOnIgnoreEvent extends AbstractBulkheadLimitEvent {
+
+ public BulkheadOnIgnoreEvent(String bulkheadName, Map eventData) {
+ super(bulkheadName, eventData);
+ }
+
+ @Override
+ public Type getEventType() {
+ return Type.IGNORED_ERROR;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s: Bulkhead '%s' error is ignored.",
+ eventData(),
+ getBulkheadName()
+ );
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnLimitDecreasedEvent.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnLimitDecreasedEvent.java
new file mode 100644
index 0000000000..94ada7139a
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnLimitDecreasedEvent.java
@@ -0,0 +1,49 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.event;
+
+import java.util.Map;
+
+/**
+ * A BulkheadEvent which informs that a limit has been decreased
+ */
+public class BulkheadOnLimitDecreasedEvent extends AbstractBulkheadLimitEvent {
+
+ public BulkheadOnLimitDecreasedEvent(String bulkheadName, Map eventData) {
+ super(bulkheadName, eventData);
+ }
+
+ @Override
+ public Type getEventType() {
+ return Type.LIMIT_DECREASED;
+ }
+
+ public int getNewMaxConcurrentCalls(){
+ return Integer.parseInt(eventData().get("newMaxConcurrentCalls"));
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s: Bulkhead '%s' limit decreased.",
+ eventData(),
+ getBulkheadName()
+ );
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnLimitIncreasedEvent.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnLimitIncreasedEvent.java
new file mode 100644
index 0000000000..c191285fd4
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnLimitIncreasedEvent.java
@@ -0,0 +1,49 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.event;
+
+import java.util.Map;
+
+/**
+ * A BulkheadEvent which informs that a limit has been increased
+ */
+public class BulkheadOnLimitIncreasedEvent extends AbstractBulkheadLimitEvent {
+
+ public BulkheadOnLimitIncreasedEvent(String bulkheadName, Map eventData) {
+ super(bulkheadName, eventData);
+ }
+
+ @Override
+ public Type getEventType() {
+ return Type.LIMIT_INCREASED;
+ }
+
+ public int getNewMaxConcurrentCalls(){
+ return Integer.parseInt(eventData().get("newMaxConcurrentCalls"));
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s: Bulkhead '%s' limit increased.",
+ eventData(),
+ getBulkheadName()
+ );
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnStateTransitionEvent.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnStateTransitionEvent.java
new file mode 100644
index 0000000000..70424bdb46
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnStateTransitionEvent.java
@@ -0,0 +1,62 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.event;
+
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+
+import java.util.Map;
+
+/**
+ * A BulkheadEvent which informs about a state transition.
+ */
+public class BulkheadOnStateTransitionEvent extends AbstractBulkheadLimitEvent {
+
+ private final AdaptiveBulkhead.State fromState;
+ private final AdaptiveBulkhead.State toState;
+
+ public BulkheadOnStateTransitionEvent(String bulkheadName, Map eventData,
+ AdaptiveBulkhead.State fromState,
+ AdaptiveBulkhead.State toState) {
+ super(bulkheadName, eventData);
+ this.fromState = fromState;
+ this.toState = toState;
+ }
+
+ @Override
+ public Type getEventType() {
+ return Type.STATE_TRANSITION;
+ }
+
+ public AdaptiveBulkhead.State getFromState() {
+ return fromState;
+ }
+
+ public AdaptiveBulkhead.State getToState() {
+ return toState;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s: Bulkhead '%s' changed state from %s to %s",
+ getCreationTime(),
+ getBulkheadName(),
+ fromState,
+ toState);
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnSuccessEvent.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnSuccessEvent.java
new file mode 100644
index 0000000000..e3f6e33ba6
--- /dev/null
+++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/event/BulkheadOnSuccessEvent.java
@@ -0,0 +1,45 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.event;
+
+import java.util.Map;
+
+/**
+ * A BulkheadEvent which informs that a call has been succeeded
+ */
+public class BulkheadOnSuccessEvent extends AbstractBulkheadLimitEvent {
+
+ public BulkheadOnSuccessEvent(String bulkheadName, Map eventData) {
+ super(bulkheadName, eventData);
+ }
+
+ @Override
+ public Type getEventType() {
+ return Type.SUCCESS;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s: Bulkhead '%s' call is succeeded.",
+ eventData(),
+ getBulkheadName()
+ );
+ }
+}
diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/internal/AdaptiveBulkhead.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/internal/AdaptiveBulkhead.java
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/BulkheadFutureTest.java b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/BulkheadFutureTest.java
index 3b3438c01e..5b283c9abf 100644
--- a/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/BulkheadFutureTest.java
+++ b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/BulkheadFutureTest.java
@@ -99,9 +99,8 @@ public void shouldDecorateFutureAndBulkheadApplyOnceOnMultipleFutureEvalFailure(
.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
Future decoratedFuture = supplier.get();
-
- catchThrowable(decoratedFuture::get);
catchThrowable(decoratedFuture::get);
+ catchThrowable(decoratedFuture::get);
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
then(helloWorldService).should(times(1)).returnHelloWorldFuture();
diff --git a/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadConfigTest.java b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadConfigTest.java
new file mode 100644
index 0000000000..28b5fb52e0
--- /dev/null
+++ b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadConfigTest.java
@@ -0,0 +1,207 @@
+package io.github.resilience4j.bulkhead.adaptive;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.BDDAssertions.then;
+import static org.assertj.core.api.Java6Assertions.assertThat;
+
+import java.time.Duration;
+import java.util.function.Predicate;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class AdaptiveBulkheadConfigTest {
+
+ @Test
+ public void testBuildCustom() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .decreaseMultiplier(0.3f)
+ .minConcurrentCalls(3)
+ .maxConcurrentCalls(3)
+ .slowCallDurationThreshold(Duration.ofMillis(150))
+ .slowCallRateThreshold(50)
+ .failureRateThreshold(50)
+ .maxWaitDuration(Duration.ofMillis(150))
+// .slidingWindowTime(5)
+ .slidingWindowSize(100)
+ .slidingWindowType(AdaptiveBulkheadConfig.SlidingWindowType.TIME_BASED)
+ .build();
+
+ assertThat(config).isNotNull();
+ assertThat(config.getDecreaseMultiplier()).isEqualTo(0.3f);
+ assertThat(config.getMinConcurrentCalls()).isEqualTo(3);
+ assertThat(config.getMaxConcurrentCalls()).isEqualTo(3);
+ assertThat(config.getMaxWaitDuration().toMillis()).isEqualTo(150);
+ assertThat(config.getSlidingWindowSize()).isEqualTo(100);
+// assertThat(config.getSlidingWindowTime()).isEqualTo(5);
+ assertThat(config.getSlowCallRateThreshold()).isEqualTo(50);
+ assertThat(config.getFailureRateThreshold()).isEqualTo(50);
+ assertThat(config.getSlidingWindowType()).isEqualTo(AdaptiveBulkheadConfig.SlidingWindowType.TIME_BASED);
+ }
+
+ @Test
+ public void tesConcurrencyDropMultiplierConfig() {
+ AdaptiveBulkheadConfig build = AdaptiveBulkheadConfig.custom()
+ .decreaseMultiplier(0.85f)
+ .build();
+ assertThat(build.getDecreaseMultiplier()).isEqualTo(0.85f);
+ }
+
+ @Test
+ public void testNotSetDesirableOperationLatencyConfig() {
+ assertThatThrownBy(() -> AdaptiveBulkheadConfig.custom()
+ .slowCallDurationThreshold(Duration.ofMillis(0))
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("slowCallDurationThreshold must be at least 1[ns]");
+ }
+
+ @Test
+ public void testNotSetMaxAcceptableRequestLatencyConfig() {
+ assertThatThrownBy(() -> AdaptiveBulkheadConfig.custom()
+ .maxConcurrentCalls(0)
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("maxConcurrentCalls must greater than 0");
+ }
+
+ @Test
+ public void testNotSetMinAcceptableRequestLatencyConfig() {
+ assertThatThrownBy(() -> AdaptiveBulkheadConfig.custom()
+ .minConcurrentCalls(0)
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("minConcurrentCalls must greater than 0");
+ }
+
+ @Ignore
+ @Test
+ public void testEqual() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .decreaseMultiplier(0.6f)
+ .minConcurrentCalls(3)
+ .maxConcurrentCalls(3)
+ .slowCallDurationThreshold(Duration.ofMillis(150))
+ .slowCallRateThreshold(50)
+ .failureRateThreshold(50)
+// .slidingWindowTime(5)
+ .slidingWindowSize(100)
+ .slidingWindowType(AdaptiveBulkheadConfig.SlidingWindowType.TIME_BASED)
+ .build();
+ AdaptiveBulkheadConfig config2 = AdaptiveBulkheadConfig.custom()
+ .decreaseMultiplier(0.6f)
+ .minConcurrentCalls(3)
+ .maxConcurrentCalls(3)
+ .slowCallDurationThreshold(Duration.ofMillis(150))
+ .slowCallRateThreshold(50)
+ .failureRateThreshold(50)
+// .slidingWindowTime(5)
+ .slidingWindowSize(100)
+ .slidingWindowType(AdaptiveBulkheadConfig.SlidingWindowType.TIME_BASED)
+ .build();
+
+ assertThat(AdaptiveBulkheadConfig.from(config2).build()).isEqualTo(config2);
+ assertThat(AdaptiveBulkheadConfig.ofDefaults().getMaxConcurrentCalls()).isEqualTo(200);
+ assertThat(config).isEqualTo(config2);
+ assertThat(config.hashCode()).isEqualTo(config2.hashCode());
+ }
+
+ @Test
+ public void shouldUseCustomIgnoreExceptionPredicate() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .ignoreException(e -> "ignore".equals(e.getMessage())).build();
+ Predicate ignoreExceptionPredicate = config.getIgnoreExceptionPredicate();
+ then(ignoreExceptionPredicate.test(new Error("ignore"))).isEqualTo(true);
+ then(ignoreExceptionPredicate.test(new Error("fail"))).isEqualTo(false);
+ then(ignoreExceptionPredicate.test(new RuntimeException("ignore"))).isEqualTo(true);
+ then(ignoreExceptionPredicate.test(new Error())).isEqualTo(false);
+ then(ignoreExceptionPredicate.test(new RuntimeException())).isEqualTo(false);
+ }
+
+ private static class ExtendsException extends Exception {
+ ExtendsException() {
+ }
+
+ ExtendsException(String message) {
+ super(message);
+ }
+ }
+
+ private static class ExtendsRuntimeException extends RuntimeException {
+ }
+
+ private static class ExtendsExtendsException extends ExtendsException {
+ }
+
+ private static class BusinessException extends Exception {
+ }
+
+ private static class ExtendsError extends Error {
+ }
+
+ @Test
+ public void shouldUseIgnoreExceptionsToBuildPredicate() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.builder(AdaptiveBulkheadConfig.ofDefaults())
+ .ignoreExceptions(RuntimeException.class, ExtendsExtendsException.class, BusinessException.class).build();
+ final Predicate super Throwable> ignoreExceptionPredicate = config.getIgnoreExceptionPredicate();
+ then(ignoreExceptionPredicate.test(new Exception())).isEqualTo(false); // not explicitly ignored
+ then(ignoreExceptionPredicate.test(new ExtendsError())).isEqualTo(false); // not explicitly ignored
+ then(ignoreExceptionPredicate.test(new ExtendsException())).isEqualTo(false); // not explicitly ignored
+ then(ignoreExceptionPredicate.test(new BusinessException())).isEqualTo(true); // explicitly ignored
+ then(ignoreExceptionPredicate.test(new RuntimeException())).isEqualTo(true); // explicitly ignored
+ then(ignoreExceptionPredicate.test(new ExtendsRuntimeException())).isEqualTo(true); // inherits ignored because of RuntimeException is ignored
+ then(ignoreExceptionPredicate.test(new ExtendsExtendsException())).isEqualTo(true); // explicitly ignored
+ }
+
+ @Ignore
+ @Test
+ public void shouldUseRecordExceptionsToBuildPredicate() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.builder(AdaptiveBulkheadConfig.ofDefaults())
+ .recordExceptions(RuntimeException.class, ExtendsExtendsException.class).build();
+ final Predicate super Throwable> failurePredicate = config.getRecordExceptionPredicate();
+ then(failurePredicate.test(new Exception())).isEqualTo(false); // not explicitly recore
+ then(failurePredicate.test(new ExtendsError())).isEqualTo(false); // not explicitly included
+ then(failurePredicate.test(new ExtendsException())).isEqualTo(false); // not explicitly included
+ then(failurePredicate.test(new BusinessException())).isEqualTo(false); // not explicitly included
+ then(failurePredicate.test(new RuntimeException())).isEqualTo(true); // explicitly included
+ then(failurePredicate.test(new ExtendsRuntimeException())).isEqualTo(true); // inherits included because RuntimeException is included
+ then(failurePredicate.test(new ExtendsExtendsException())).isEqualTo(true); // explicitly included
+ }
+
+ @Test
+ public void shouldCreateCombinedRecordExceptionPredicate() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.builder(AdaptiveBulkheadConfig.ofDefaults())
+ .recordException(e -> "test".equals(e.getMessage())) //1
+ .recordExceptions(RuntimeException.class, ExtendsExtendsException.class) //2
+ .build();
+ final Predicate super Throwable> recordExceptionPredicate = config.getRecordExceptionPredicate();
+ then(recordExceptionPredicate.test(new Exception())).isEqualTo(false); // not explicitly included
+ then(recordExceptionPredicate.test(new Exception("test"))).isEqualTo(true); // explicitly included by 1
+ then(recordExceptionPredicate.test(new ExtendsError())).isEqualTo(false); // not explicitly included
+ then(recordExceptionPredicate.test(new ExtendsException())).isEqualTo(false); // explicitly excluded by 3
+ then(recordExceptionPredicate.test(new ExtendsException("test"))).isEqualTo(true); // explicitly included by 1
+ then(recordExceptionPredicate.test(new BusinessException())).isEqualTo(false); // not explicitly included
+ then(recordExceptionPredicate.test(new RuntimeException())).isEqualTo(true); // explicitly included by 2
+ then(recordExceptionPredicate.test(new ExtendsRuntimeException())).isEqualTo(true); // implicitly included by RuntimeException
+ then(recordExceptionPredicate.test(new ExtendsExtendsException())).isEqualTo(true); // explicitly included
+ }
+
+ @Test
+ public void shouldCreateCombinedIgnoreExceptionPredicate() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.builder(AdaptiveBulkheadConfig.ofDefaults())
+ .ignoreException(e -> "ignore".equals(e.getMessage())) //1
+ .ignoreExceptions(BusinessException.class, ExtendsExtendsException.class, ExtendsRuntimeException.class) //2
+ .build();
+ final Predicate super Throwable> ignoreExceptionPredicate = config.getIgnoreExceptionPredicate();
+ then(ignoreExceptionPredicate.test(new Exception())).isEqualTo(false); // not explicitly ignored
+ then(ignoreExceptionPredicate.test(new Exception("ignore"))).isEqualTo(true); // explicitly ignored by 1
+ then(ignoreExceptionPredicate.test(new ExtendsError())).isEqualTo(false); // not explicitly ignored
+ then(ignoreExceptionPredicate.test(new ExtendsException())).isEqualTo(false); // not explicitly ignored
+ then(ignoreExceptionPredicate.test(new ExtendsException("ignore"))).isEqualTo(true); // explicitly ignored 1
+ then(ignoreExceptionPredicate.test(new BusinessException())).isEqualTo(true); // explicitly ignored 2
+ then(ignoreExceptionPredicate.test(new RuntimeException())).isEqualTo(false); // not explicitly ignored
+ then(ignoreExceptionPredicate.test(new ExtendsRuntimeException())).isEqualTo(true); // explicitly ignored 2
+ then(ignoreExceptionPredicate.test(new ExtendsExtendsException())).isEqualTo(true); // implicitly ignored by ExtendsRuntimeException
+ }
+
+}
\ No newline at end of file
diff --git a/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadFutureTest.java b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadFutureTest.java
new file mode 100644
index 0000000000..e731c4ec76
--- /dev/null
+++ b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadFutureTest.java
@@ -0,0 +1,188 @@
+package io.github.resilience4j.bulkhead.adaptive;
+
+import io.github.resilience4j.bulkhead.BulkheadFullException;
+import io.github.resilience4j.test.HelloWorldService;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.*;
+import java.util.function.Supplier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+
+public class AdaptiveBulkheadFutureTest {
+
+ private HelloWorldService helloWorldService;
+ private Future future;
+ private AdaptiveBulkheadConfig config;
+
+ @Before
+ public void setUp() {
+ helloWorldService = mock(HelloWorldService.class);
+ future = mock(Future.class);
+ config = AdaptiveBulkheadConfig.custom()
+ .maxConcurrentCalls(1)
+ .initialConcurrentCalls(1)
+ .build();
+ }
+
+ @Test
+ public void shouldDecorateSupplierAndReturnWithSuccess() throws Exception {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(future.get()).willReturn("Hello world");
+ given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
+ Supplier> supplier = AdaptiveBulkhead
+ .decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
+
+ String result = supplier.get().get();
+
+ assertThat(result).isEqualTo("Hello world");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should(times(1)).returnHelloWorldFuture();
+ then(future).should(times(1)).get();
+ }
+
+ @Test
+ public void shouldDecorateSupplierAndReturnWithSuccessAndTimeout() throws Exception {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(future.get(anyLong(), any(TimeUnit.class))).willReturn("Hello world");
+ given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
+ Supplier> supplier = AdaptiveBulkhead
+ .decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
+
+ String result = supplier.get().get(5, TimeUnit.SECONDS);
+
+ assertThat(result).isEqualTo("Hello world");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should(times(1)).returnHelloWorldFuture();
+ then(future).should(times(1)).get(anyLong(), any(TimeUnit.class));
+ }
+
+ @Test
+ public void shouldDecorateFutureAndBulkheadApplyOnceOnMultipleFutureEval() throws Exception {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(future.get(anyLong(), any(TimeUnit.class))).willReturn("Hello world");
+ given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
+ Supplier> supplier = AdaptiveBulkhead
+ .decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
+
+ Future decoratedFuture = supplier.get();
+ decoratedFuture.get(5, TimeUnit.SECONDS);
+ decoratedFuture.get(5, TimeUnit.SECONDS);
+
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should(times(1)).returnHelloWorldFuture();
+ then(future).should(times(2)).get(anyLong(), any(TimeUnit.class));
+ }
+
+ @Test
+ public void shouldDecorateFutureAndBulkheadApplyOnceOnMultipleFutureEvalFailure() throws Exception {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(future.get()).willThrow(new ExecutionException(new RuntimeException("Hello world")));
+ given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
+ Supplier> supplier = AdaptiveBulkhead
+ .decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
+
+ Future decoratedFuture = supplier.get();
+ catchThrowable(decoratedFuture::get);
+ catchThrowable(decoratedFuture::get);
+
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should(times(1)).returnHelloWorldFuture();
+ then(future).should(times(2)).get();
+ }
+
+ @Test
+ public void shouldDecorateSupplierAndReturnWithExceptionAtAsyncStage() throws Exception {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(future.get()).willThrow(new ExecutionException(new RuntimeException("BAM!")));
+ given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
+ Supplier> supplier = AdaptiveBulkhead
+ .decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
+
+ Throwable thrown = catchThrowable(() -> supplier.get().get());
+
+ assertThat(thrown).isInstanceOf(ExecutionException.class)
+ .hasCauseInstanceOf(RuntimeException.class);
+ assertThat(thrown.getCause().getMessage()).isEqualTo("BAM!");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should(times(1)).returnHelloWorldFuture();
+ then(future).should(times(1)).get();
+ }
+
+ @Test
+ public void shouldDecorateSupplierAndReturnWithExceptionAtSyncStage() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldFuture()).willThrow(new RuntimeException("BAM!"));
+
+ Supplier> supplier = AdaptiveBulkhead
+ .decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
+
+ Throwable thrown = catchThrowable(() -> supplier.get().get());
+ assertThat(thrown).isInstanceOf(RuntimeException.class)
+ .hasMessage("BAM!");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should(times(1)).returnHelloWorldFuture();
+ then(future).shouldHaveZeroInteractions();
+ }
+
+ @Test
+ public void shouldReturnFailureWithBulkheadFullException() throws Exception {
+ // tag::bulkheadFullException[]
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom().maxConcurrentCalls(2).build();
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ bulkhead.tryAcquirePermission();
+ bulkhead.tryAcquirePermission();
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(0);
+ given(future.get()).willReturn("Hello world");
+ given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
+ Supplier> supplier = AdaptiveBulkhead.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
+
+ Throwable thrown = catchThrowable(() -> supplier.get().get());
+
+ assertThat(thrown).isInstanceOf(ExecutionException.class)
+ .hasCauseInstanceOf(BulkheadFullException.class);
+ then(helloWorldService).shouldHaveZeroInteractions();
+ then(future).shouldHaveZeroInteractions();
+ // end::bulkheadFullException[]
+ }
+
+ @Test
+ public void shouldReturnFailureWithFutureCancellationException() throws Exception {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(future.get()).willThrow(new CancellationException());
+ given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
+ Supplier> supplier = AdaptiveBulkhead
+ .decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
+
+ Throwable thrown = catchThrowable(() -> supplier.get().get());
+
+ assertThat(thrown).isInstanceOf(CancellationException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should(times(1)).returnHelloWorldFuture();
+ then(future).should(times(1)).get();
+ }
+
+ @Test
+ public void shouldReturnFailureWithFutureTimeoutException() throws Exception {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(future.get(anyLong(), any(TimeUnit.class))).willThrow(new TimeoutException());
+ given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
+ Supplier> supplier = AdaptiveBulkhead
+ .decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
+
+ Throwable thrown = catchThrowable(() -> supplier.get().get(5, TimeUnit.SECONDS));
+
+ assertThat(thrown).isInstanceOf(TimeoutException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should(times(1)).returnHelloWorldFuture();
+ then(future).should(times(1)).get(anyLong(), any(TimeUnit.class));
+ }
+}
diff --git a/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadTest.java b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadTest.java
new file mode 100644
index 0000000000..6b13195a0c
--- /dev/null
+++ b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/AdaptiveBulkheadTest.java
@@ -0,0 +1,669 @@
+package io.github.resilience4j.bulkhead.adaptive;
+
+import io.github.resilience4j.bulkhead.BulkheadFullException;
+import io.github.resilience4j.core.functions.CheckedConsumer;
+import io.github.resilience4j.core.functions.CheckedFunction;
+import io.github.resilience4j.core.functions.CheckedRunnable;
+import io.github.resilience4j.core.functions.CheckedSupplier;
+import io.github.resilience4j.test.HelloWorldService;
+import io.vavr.control.Try;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import javax.xml.ws.WebServiceException;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.never;
+
+public class AdaptiveBulkheadTest {
+
+ private AdaptiveBulkhead bulkhead;
+ private AdaptiveBulkheadConfig config;
+ private HelloWorldService helloWorldService;
+
+ @Before
+ public void setUp() {
+ helloWorldService = Mockito.mock(HelloWorldService.class);
+ config = AdaptiveBulkheadConfig.custom()
+ .maxConcurrentCalls(2)
+ .minConcurrentCalls(1)
+ .initialConcurrentCalls(1)
+ .slowCallDurationThreshold(Duration.ofMillis(100))
+ .build();
+ bulkhead = AdaptiveBulkhead.of("test", config);
+ }
+
+ @Test
+ public void shouldReturnTheCorrectName() {
+ assertThat(bulkhead.getName()).isEqualTo("test");
+ }
+
+ @Test
+ public void testToString() {
+ assertThat(bulkhead.toString()).isEqualTo("AdaptiveBulkhead 'test'");
+ }
+
+ @Test
+ public void testCreateWithNullConfig() {
+ assertThatThrownBy(() -> AdaptiveBulkhead.of("test", () -> null))
+ .isInstanceOf(NullPointerException.class).hasMessage("Config must not be null");
+ }
+
+ @Test
+ public void testCreateWithDefaults() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.ofDefaults("test");
+
+ assertThat(bulkhead).isNotNull();
+ assertThat(bulkhead.getBulkheadConfig()).isNotNull();
+ assertThat(bulkhead.getBulkheadConfig().getSlidingWindowSize()).isEqualTo(100);
+ }
+
+ @Test
+ public void shouldDecorateSupplierAndReturnWithSuccess() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorld()).willReturn("Hello world");
+
+ Supplier supplier = AdaptiveBulkhead
+ .decorateSupplier(bulkhead, helloWorldService::returnHelloWorld);
+
+ assertThat(supplier.get()).isEqualTo("Hello world");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().returnHelloWorld();
+ }
+
+ @Test
+ public void shouldExecuteSupplierAndReturnWithSuccess() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorld()).willReturn("Hello world");
+
+ String result = bulkhead.executeSupplier(helloWorldService::returnHelloWorld);
+
+ assertThat(result).isEqualTo("Hello world");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().returnHelloWorld();
+ }
+
+ @Test
+ public void shouldDecorateSupplierAndReturnWithException() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorld())
+ .willThrow(new RuntimeException("BAM!"));
+ Supplier supplier = AdaptiveBulkhead
+ .decorateSupplier(bulkhead, helloWorldService::returnHelloWorld);
+
+ Try result = Try.of(supplier::get);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().returnHelloWorld();
+ }
+
+ @Test
+ public void shouldDecorateSupplierAndReturnWithExceptionAdaptIfError() {
+ final AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .minConcurrentCalls(2)
+ .slowCallDurationThreshold(Duration.ofMillis(200))
+ .recordExceptions(RuntimeException.class)
+ .build();
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorld())
+ .willThrow(new RuntimeException("BAM!"));
+ Supplier supplier = AdaptiveBulkhead
+ .decorateSupplier(bulkhead, helloWorldService::returnHelloWorld);
+
+ Try result = Try.of(supplier::get);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(2);
+ then(helloWorldService).should().returnHelloWorld();
+ }
+
+ @Test
+ public void shouldDecorateCheckedSupplierAndReturnWithSuccess() throws Throwable {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithException())
+ .willReturn("Hello world");
+ CheckedSupplier checkedSupplier = AdaptiveBulkhead
+ .decorateCheckedSupplier(bulkhead, helloWorldService::returnHelloWorldWithException);
+
+ String result = checkedSupplier.get();
+
+ assertThat(result).isEqualTo("Hello world");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().returnHelloWorldWithException();
+ }
+
+ @Test
+ public void shouldDecorateCheckedSupplierAndReturnWithException() throws Throwable {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithException())
+ .willThrow(new RuntimeException("BAM!"));
+ CheckedSupplier checkedSupplier = AdaptiveBulkhead
+ .decorateCheckedSupplier(bulkhead, helloWorldService::returnHelloWorldWithException);
+
+ Try result = Try.of(checkedSupplier::get);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().returnHelloWorldWithException();
+ }
+
+ @Test
+ public void shouldDecorateCheckedSupplierAndReturnWithExceptionAdaptIfError() throws Throwable {
+ final AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .minConcurrentCalls(2)
+ .slowCallDurationThreshold(Duration.ofMillis(200))
+ .recordExceptions(RuntimeException.class)
+ .build();
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithException())
+ .willThrow(new RuntimeException("BAM!"));
+ CheckedSupplier checkedSupplier = AdaptiveBulkhead
+ .decorateCheckedSupplier(bulkhead, helloWorldService::returnHelloWorldWithException);
+
+ Try result = Try.of(checkedSupplier::get);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(2);
+ then(helloWorldService).should().returnHelloWorldWithException();
+ }
+
+ @Test
+ public void shouldDecorateCallableAndReturnWithSuccess() throws Throwable {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithException())
+ .willReturn("Hello world");
+ Callable callable = AdaptiveBulkhead
+ .decorateCallable(bulkhead, helloWorldService::returnHelloWorldWithException);
+
+ String result = callable.call();
+
+ assertThat(result).isEqualTo("Hello world");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().returnHelloWorldWithException();
+ }
+
+ @Test
+ public void shouldExecuteCallableAndReturnWithSuccess() throws Throwable {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithException())
+ .willReturn("Hello world");
+
+ String result = bulkhead.executeCallable(helloWorldService::returnHelloWorldWithException);
+
+ assertThat(result).isEqualTo("Hello world");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().returnHelloWorldWithException();
+ }
+
+ @Test
+ public void shouldDecorateCallableAndReturnWithException() throws Throwable {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithException())
+ .willThrow(new RuntimeException("BAM!"));
+ Callable callable = AdaptiveBulkhead
+ .decorateCallable(bulkhead, helloWorldService::returnHelloWorldWithException);
+
+ Try result = Try.of(callable::call);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().returnHelloWorldWithException();
+ }
+
+ @Test
+ public void shouldDecorateCallableAndReturnWithExceptionIfError() throws Throwable {
+ final AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .minConcurrentCalls(2)
+ .slowCallDurationThreshold(Duration.ofMillis(200))
+ .recordExceptions(RuntimeException.class)
+ .build();
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithException())
+ .willThrow(new RuntimeException("BAM!"));
+ Callable callable = AdaptiveBulkhead
+ .decorateCallable(bulkhead, helloWorldService::returnHelloWorldWithException);
+
+ Try result = Try.of(callable::call);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(2);
+ then(helloWorldService).should().returnHelloWorldWithException();
+ }
+
+ @Test
+ public void shouldDecorateCheckedRunnableAndReturnWithSuccess() throws Throwable {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+
+ AdaptiveBulkhead
+ .decorateCheckedRunnable(bulkhead, helloWorldService::sayHelloWorldWithException)
+ .run();
+
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().sayHelloWorldWithException();
+ }
+
+ @Test
+ public void shouldDecorateCheckedRunnableAndReturnWithException() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ CheckedRunnable checkedRunnable = AdaptiveBulkhead.decorateCheckedRunnable(bulkhead, () -> {
+ throw new RuntimeException("BAM!");
+ });
+
+ Try result = Try.run(checkedRunnable::run);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldDecorateCheckedRunnableAndReturnWithExceptionAdaptIfError() {
+ final AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .minConcurrentCalls(2)
+ .slowCallDurationThreshold(Duration.ofMillis(200))
+ .recordExceptions(RuntimeException.class)
+ .build();
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ CheckedRunnable checkedRunnable = AdaptiveBulkhead.decorateCheckedRunnable(bulkhead, () -> {
+ throw new RuntimeException("BAM!");
+ });
+
+ Try result = Try.run(checkedRunnable::run);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(2);
+ }
+
+ @Test
+ public void shouldDecorateRunnableAndReturnWithSuccess() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+
+ AdaptiveBulkhead.decorateRunnable(bulkhead, helloWorldService::sayHelloWorld).run();
+
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().sayHelloWorld();
+ }
+
+ @Test
+ public void shouldExecuteRunnableAndReturnWithSuccess() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+
+ bulkhead.executeRunnable(helloWorldService::sayHelloWorld);
+
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().sayHelloWorld();
+ }
+
+ @Test
+ public void shouldDecorateRunnableAndReturnWithException() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ Runnable runnable = AdaptiveBulkhead.decorateRunnable(bulkhead, () -> {
+ throw new RuntimeException("BAM!");
+ });
+
+ Try result = Try.run(runnable::run);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldDecorateRunnableAndReturnWithExceptionAdaptIfError() {
+ final AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .minConcurrentCalls(2)
+ .slowCallDurationThreshold(Duration.ofMillis(200))
+ .recordExceptions(RuntimeException.class)
+ .build();
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ Runnable runnable = AdaptiveBulkhead.decorateRunnable(bulkhead, () -> {
+ throw new RuntimeException("BAM!");
+ });
+
+ Try result = Try.run(runnable::run);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(2);
+ }
+
+ @Test
+ public void shouldDecorateConsumerAndReturnWithSuccess() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+
+ AdaptiveBulkhead.decorateConsumer(bulkhead, helloWorldService::sayHelloWorldWithName)
+ .accept("Tom");
+
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().sayHelloWorldWithName("Tom");
+ }
+
+ @Test
+ public void shouldDecorateConsumerAndReturnWithException() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ Consumer consumer = AdaptiveBulkhead.decorateConsumer(bulkhead, (value) -> {
+ throw new RuntimeException("BAM!");
+ });
+
+ Try result = Try.run(() -> consumer.accept("Tom"));
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldDecorateConsumerAndReturnWithExceptionAdaptIfError() {
+ final AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .minConcurrentCalls(2)
+ .slowCallDurationThreshold(Duration.ofMillis(100))
+ .recordExceptions(RuntimeException.class)
+ .build();
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ Consumer consumer = AdaptiveBulkhead.decorateConsumer(bulkhead, (value) -> {
+ throw new RuntimeException("BAM!");
+ });
+
+ Try result = Try.run(() -> consumer.accept("Tom"));
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(2);
+ }
+
+ @Test
+ public void shouldDecorateCheckedConsumerAndReturnWithSuccess() throws Throwable {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+
+ AdaptiveBulkhead.decorateCheckedConsumer(bulkhead,
+ helloWorldService::sayHelloWorldWithNameWithException)
+ .accept("Tom");
+
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().sayHelloWorldWithNameWithException("Tom");
+ }
+
+ @Test
+ public void shouldDecorateCheckedConsumerAndReturnWithException() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ CheckedConsumer checkedConsumer = AdaptiveBulkhead
+ .decorateCheckedConsumer(bulkhead, (value) -> {
+ throw new RuntimeException("BAM!");
+ });
+
+ Try result = Try.run(() -> checkedConsumer.accept("Tom"));
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldDecorateFunctionAndReturnWithSuccess() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithName("Tom"))
+ .willReturn("Hello world Tom");
+ Function function = AdaptiveBulkhead
+ .decorateFunction(bulkhead, helloWorldService::returnHelloWorldWithName);
+
+ String result = function.apply("Tom");
+
+ assertThat(result).isEqualTo("Hello world Tom");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().returnHelloWorldWithName("Tom");
+ }
+
+ @Test
+ public void shouldDecorateFunctionAndReturnWithException() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithName("Tom"))
+ .willThrow(new RuntimeException("BAM!"));
+ Function function = AdaptiveBulkhead
+ .decorateFunction(bulkhead, helloWorldService::returnHelloWorldWithName);
+
+ Try result = Try.of(() -> function.apply("Tom"));
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldDecorateCheckedFunctionAndReturnWithSuccess() throws Throwable {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithNameWithException("Tom"))
+ .willReturn("Hello world Tom");
+
+ String result = AdaptiveBulkhead.decorateCheckedFunction(bulkhead,
+ helloWorldService::returnHelloWorldWithNameWithException)
+ .apply("Tom");
+
+ assertThat(result).isEqualTo("Hello world Tom");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should()
+ .returnHelloWorldWithNameWithException("Tom");
+ }
+
+ @Test
+ public void shouldDecorateCheckedFunctionAndReturnWithException() throws IOException {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorldWithNameWithException("Tom"))
+ .willThrow(new RuntimeException("BAM!"));
+ CheckedFunction function = AdaptiveBulkhead
+ .decorateCheckedFunction(bulkhead,
+ helloWorldService::returnHelloWorldWithNameWithException);
+
+ Try result = Try.of(() -> function.apply("Tom"));
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldReturnFailureWithBulkheadFullException() {
+ // tag::bulkheadFullException[]
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .minConcurrentCalls(2)
+ .slowCallDurationThreshold(Duration.ofMillis(2))
+ .build();
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ bulkhead.acquirePermission();
+ bulkhead.acquirePermission();
+ CheckedRunnable checkedRunnable = AdaptiveBulkhead.decorateCheckedRunnable(bulkhead, () -> {
+ throw new RuntimeException("BAM!");
+ });
+
+ Try> result = Try.run(checkedRunnable::run);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(BulkheadFullException.class);
+ // end::bulkheadFullException[]
+ }
+
+ @Test
+ public void shouldReturnFailureWithRuntimeException() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .minConcurrentCalls(2)
+ .slowCallDurationThreshold(Duration.ofMillis(2))
+ .build();
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ bulkhead.tryAcquirePermission();
+ CheckedRunnable checkedRunnable = AdaptiveBulkhead.decorateCheckedRunnable(bulkhead, () -> {
+ throw new RuntimeException("BAM!");
+ });
+
+ Try> result = Try.run(checkedRunnable::run);
+
+ assertThat(result.isFailure()).isTrue();
+ assertThat(result.failed().get()).isInstanceOf(RuntimeException.class);
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldInvokeAsyncApply() throws ExecutionException, InterruptedException {
+ // tag::shouldInvokeAsyncApply[]
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ Supplier decoratedSupplier = AdaptiveBulkhead
+ .decorateSupplier(bulkhead, () -> "This can be any method which returns: 'Hello");
+ CompletableFuture future = CompletableFuture.supplyAsync(decoratedSupplier)
+ .thenApply(value -> value + " world'");
+
+ String result = future.get();
+
+ assertThat(result).isEqualTo("This can be any method which returns: 'Hello world'");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ // end::shouldInvokeAsyncApply[]
+ }
+
+ @Test
+ public void shouldDecorateCompletionStageAndReturnWithSuccess()
+ throws ExecutionException, InterruptedException {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorld()).willReturn("Hello");
+ Supplier> completionStageSupplier =
+ () -> CompletableFuture.supplyAsync(helloWorldService::returnHelloWorld);
+ Supplier> decoratedCompletionStageSupplier =
+ AdaptiveBulkhead.decorateCompletionStage(bulkhead, completionStageSupplier);
+
+ CompletionStage decoratedCompletionStage = decoratedCompletionStageSupplier.get()
+ .thenApply(value -> value + " world");
+
+ assertThat(decoratedCompletionStage.toCompletableFuture().get()).isEqualTo("Hello world");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ then(helloWorldService).should().returnHelloWorld();
+ }
+
+ @Test
+ public void shouldDecorateCompletionStageAndReturnWithExceptionAtSyncStage() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ Supplier> completionStageSupplier = () -> {
+ throw new WebServiceException("BAM! At sync stage");
+ };
+ Supplier> decoratedCompletionStageSupplier =
+ AdaptiveBulkhead.decorateCompletionStage(bulkhead, completionStageSupplier);
+
+ // NOTE: Try.of does not detect a completion stage that has been completed with failure !
+ Try> result = Try.of(decoratedCompletionStageSupplier::get);
+
+ // Then the helloWorldService should be invoked 0 times
+ then(helloWorldService).should(never()).returnHelloWorld();
+ assertThat(result.isSuccess()).isTrue();
+ result.get()
+ .exceptionally(
+ error -> {
+ // NOTE: Try.of does not detect a completion stage that has been completed with failure !
+ assertThat(error).isInstanceOf(WebServiceException.class);
+ return null;
+ }
+ );
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldDecorateCompletionStageAndReturnWithExceptionAtSyncStageAdaptIfError() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .minConcurrentCalls(2)
+ .slowCallDurationThreshold(Duration.ofMillis(2))
+ .build();
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ Supplier> completionStageSupplier = () -> {
+ throw new WebServiceException("BAM! At sync stage");
+ };
+ Supplier> decoratedCompletionStageSupplier =
+ AdaptiveBulkhead.decorateCompletionStage(bulkhead, completionStageSupplier);
+
+ // NOTE: Try.of does not detect a completion stage that has been completed with failure !
+ Try> result = Try.of(decoratedCompletionStageSupplier::get);
+
+ then(helloWorldService).should(never()).returnHelloWorld();
+ assertThat(result.isSuccess()).isTrue();
+ result.get()
+ .exceptionally(
+ error -> {
+ // NOTE: Try.of does not detect a completion stage that has been completed with failure !
+ assertThat(error).isInstanceOf(WebServiceException.class);
+ return null;
+ }
+ );
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(2);
+ }
+
+ @Test
+ public void shouldDecorateCompletionStageAndReturnWithExceptionAtAsyncStage() {
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ given(helloWorldService.returnHelloWorld())
+ .willThrow(new RuntimeException("BAM! At async stage"));
+ Supplier> completionStageSupplier =
+ () -> CompletableFuture.supplyAsync(helloWorldService::returnHelloWorld);
+ Supplier> decoratedCompletionStageSupplier =
+ AdaptiveBulkhead.decorateCompletionStage(bulkhead, completionStageSupplier);
+
+ CompletionStage decoratedCompletionStage = decoratedCompletionStageSupplier.get();
+
+ assertThatThrownBy(decoratedCompletionStage.toCompletableFuture()::get)
+ .isInstanceOf(ExecutionException.class)
+ .hasCause(new RuntimeException("BAM! At async stage"));
+ then(helloWorldService).should().returnHelloWorld();
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldChainDecoratedFunctions() {
+ // tag::shouldChainDecoratedFunctions[]
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("test", config);
+ AdaptiveBulkhead anotherBulkhead = AdaptiveBulkhead.of("testAnother", config);
+ // Given a Supplier and a Function which are decorated by different bulkheads
+ CheckedSupplier decoratedSupplier = AdaptiveBulkhead
+ .decorateCheckedSupplier(bulkhead, () -> "Hello");
+ CheckedFunction decoratedFunction = AdaptiveBulkhead
+ .decorateCheckedFunction(anotherBulkhead, (input) -> input + " world");
+
+ Try result = Try.of(decoratedSupplier::get).mapTry(decoratedFunction::apply);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.get()).isEqualTo("Hello world");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ assertThat(anotherBulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ // end::shouldChainDecoratedFunctions[]
+ }
+
+
+ @Test
+ public void shouldInvokeMap() {
+ // tag::shouldInvokeMap[]
+ AdaptiveBulkhead bulkhead = AdaptiveBulkhead.of("testName", config);
+ CheckedSupplier decoratedSupplier = AdaptiveBulkhead.decorateCheckedSupplier(
+ bulkhead, () -> "This can be any method which returns: 'Hello");
+
+ Try result = Try.of(decoratedSupplier::get)
+ .map(value -> value + " world'");
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.get()).isEqualTo("This can be any method which returns: 'Hello world'");
+ assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
+ // end::shouldInvokeMap[]
+ }
+
+}
diff --git a/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadEventPublisherTest.java b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadEventPublisherTest.java
new file mode 100644
index 0000000000..a95e6edcad
--- /dev/null
+++ b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadEventPublisherTest.java
@@ -0,0 +1,98 @@
+/*
+ *
+ * Copyright 2019 Mahmoud Romeh
+ *
+ * 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
+ *
+ * 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.github.resilience4j.bulkhead.adaptive.internal;
+
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkheadConfig;
+import io.github.resilience4j.bulkhead.event.AdaptiveBulkheadEvent;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Java6Assertions.assertThat;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+
+
+public class AdaptiveBulkheadEventPublisherTest {
+
+ private Logger logger;
+ private AdaptiveBulkhead adaptiveBulkhead;
+
+ @Before
+ public void setUp() {
+ logger = mock(Logger.class);
+ adaptiveBulkhead = AdaptiveBulkhead.ofDefaults("testName");
+ }
+
+ @Test
+ public void shouldReturnTheSameConsumer() {
+ AdaptiveBulkhead.AdaptiveEventPublisher eventPublisher = adaptiveBulkhead.getEventPublisher();
+ AdaptiveBulkhead.AdaptiveEventPublisher eventPublisher2 = adaptiveBulkhead.getEventPublisher();
+
+ assertThat(eventPublisher).isEqualTo(eventPublisher2);
+ }
+
+
+ @Test
+ public void shouldConsumeOnSuccessEvent() {
+ adaptiveBulkhead.getEventPublisher()
+ .onSuccess(this::logEventType);
+
+ adaptiveBulkhead.onSuccess(1000, TimeUnit.NANOSECONDS);
+
+ then(logger).should(times(1)).info("SUCCESS");
+ }
+
+ @Test
+ public void shouldConsumeOnErrorEvent() {
+ adaptiveBulkhead.getEventPublisher()
+ .onError(this::logEventType);
+
+ adaptiveBulkhead.onError(1000, TimeUnit.NANOSECONDS, new IOException("BAM!"));
+
+ then(logger).should(times(1)).info("ERROR");
+ }
+
+
+ private void logEventType(AdaptiveBulkheadEvent event) {
+ logger.info(event.getEventType().toString());
+ }
+
+ @Test
+ public void shouldConsumeIgnoredErrorEvent() {
+ AdaptiveBulkheadConfig adaptiveBulkheadConfig = AdaptiveBulkheadConfig.custom()
+ .ignoreExceptions(IOException.class)
+ .build();
+
+ adaptiveBulkhead = AdaptiveBulkhead.of("test", adaptiveBulkheadConfig);
+
+ adaptiveBulkhead.getEventPublisher()
+ .onIgnoredError(this::logEventType);
+
+ adaptiveBulkhead.onError(10000, TimeUnit.NANOSECONDS, new IOException("BAM!"));
+
+ then(logger).should(times(1)).info("IGNORED_ERROR");
+ }
+
+}
diff --git a/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadGraphTest.java b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadGraphTest.java
new file mode 100644
index 0000000000..47e72fc872
--- /dev/null
+++ b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadGraphTest.java
@@ -0,0 +1,190 @@
+package io.github.resilience4j.bulkhead.adaptive.internal;
+
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkheadConfig;
+import io.github.resilience4j.bulkhead.event.AbstractBulkheadLimitEvent;
+import org.junit.Before;
+import org.junit.Test;
+import org.knowm.xchart.*;
+import org.knowm.xchart.style.AxesChartStyler;
+import org.knowm.xchart.style.Styler;
+import org.knowm.xchart.style.markers.None;
+
+import java.awt.*;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+public class AdaptiveBulkheadGraphTest {
+
+ private static final boolean DRAW_GRAPHS = false;
+ private static final int CALLS = 300;
+ private static final Random NON_RANDOM = new Random(0);
+ private static final int SLOW_CALL_DURATION_THRESHOLD = 200;
+ private static final int RATE_THRESHOLD = 50;
+ private static final int MAX_CONCURRENT_CALLS = 100;
+ private static final int MIN_CONCURRENT_CALLS = 10;
+ private static final int INITIAL_CONCURRENT_CALLS = 30;
+ private AdaptiveBulkheadStateMachine bulkhead;
+ private List time = new ArrayList<>();
+ private List concurrencyLimitData = new ArrayList<>();
+ private List slowCallsRateData = new ArrayList<>();
+ private List errorCallsRateData = new ArrayList<>();
+
+ @Before
+ public void setup() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .increaseSummand(5)
+ .maxConcurrentCalls(MAX_CONCURRENT_CALLS)
+ .minConcurrentCalls(MIN_CONCURRENT_CALLS)
+ .initialConcurrentCalls(INITIAL_CONCURRENT_CALLS)
+ .minimumNumberOfCalls(5)
+ .slidingWindowSize(5)
+ .slidingWindowType(AdaptiveBulkheadConfig.SlidingWindowType.TIME_BASED)
+ .failureRateThreshold(RATE_THRESHOLD)
+ .slowCallRateThreshold(RATE_THRESHOLD)
+ .slowCallDurationThreshold(Duration.ofMillis(SLOW_CALL_DURATION_THRESHOLD))
+ .build();
+ bulkhead = (AdaptiveBulkheadStateMachine) AdaptiveBulkhead.of("test", config);
+ bulkhead.getEventPublisher().onSuccess(this::recordCallStats);
+ bulkhead.getEventPublisher().onError(this::recordCallStats);
+ }
+
+ @Test
+ public void testSlowCalls() {
+ for (int i = 0; i < CALLS; i++) {
+ bulkhead.onSuccess(nextLatency(), TimeUnit.MILLISECONDS);
+ }
+ drawGraph("testSlowCalls");
+ }
+
+ @Test
+ public void testFailedCalls() {
+ Throwable failure = new Throwable();
+
+ for (int i = 0; i < CALLS; i++) {
+ if (nextErrorOccurred()) {
+ bulkhead.onError(1, TimeUnit.MILLISECONDS, failure);
+ } else {
+ bulkhead.onSuccess(1, TimeUnit.MILLISECONDS);
+ }
+ }
+ drawGraph("testFailedCalls");
+ }
+
+ @Test
+ public void testFailedCallsOnly() {
+ Throwable failure = new Throwable();
+
+ for (int i = 0; i < CALLS; i++) {
+ bulkhead.onError(1, TimeUnit.MILLISECONDS, failure);
+ }
+ drawGraph("testFailedCallsOnly");
+ }
+
+ @Test
+ public void testSuccessfulCallsOnly() {
+ for (int i = 0; i < CALLS; i++) {
+ bulkhead.onSuccess(1, TimeUnit.MILLISECONDS);
+ }
+ drawGraph("testSuccessfulCallsOnly");
+ }
+
+ @Test
+ public void testSlowCallsWithLowMinConcurrentCalls() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .increaseSummand(1)
+ .maxConcurrentCalls(MAX_CONCURRENT_CALLS)
+ .minConcurrentCalls(10)
+ .initialConcurrentCalls(20)
+ .minimumNumberOfCalls(1)
+ .slidingWindowType(AdaptiveBulkheadConfig.SlidingWindowType.TIME_BASED)
+ .failureRateThreshold(RATE_THRESHOLD)
+ .slowCallRateThreshold(RATE_THRESHOLD)
+ .slowCallDurationThreshold(Duration.ofMillis(SLOW_CALL_DURATION_THRESHOLD))
+ .build();
+ bulkhead = (AdaptiveBulkheadStateMachine) AdaptiveBulkhead
+ .of("test", config);
+ bulkhead.getEventPublisher().onSuccess(this::recordCallStats);
+ bulkhead.getEventPublisher().onError(this::recordCallStats);
+ Throwable failure = new Throwable();
+
+ for (int j = 0; j < 3; j++) {
+ for (int i = 0; i < 5; i++) {
+ bulkhead.onSuccess(1, TimeUnit.MILLISECONDS);
+ }
+ for (int i = 0; i < 3; i++) {
+ bulkhead.onError(System.currentTimeMillis(), TimeUnit.MILLISECONDS, failure);
+ }
+ }
+ drawGraph("WithLowMinConcurrentCalls");
+ }
+
+ private int nextLatency() {
+ return NON_RANDOM.nextInt((int) (1.8f * SLOW_CALL_DURATION_THRESHOLD));
+ }
+
+ private boolean nextErrorOccurred() {
+ return NON_RANDOM.nextInt(2) == 0;
+ }
+
+ private void recordCallStats(AbstractBulkheadLimitEvent event) {
+ AdaptiveBulkheadMetrics metrics = (AdaptiveBulkheadMetrics) bulkhead.getMetrics();
+ concurrencyLimitData.add(metrics.getMaxAllowedConcurrentCalls());
+ time.add((double) concurrencyLimitData.size());
+ slowCallsRateData.add(MAX_CONCURRENT_CALLS * metrics.getSnapshot().getSlowCallRate() / 100);
+ errorCallsRateData.add(MAX_CONCURRENT_CALLS * metrics.getSnapshot().getFailureRate() / 100);
+ }
+
+ private void drawGraph(String testName) {
+ if (!DRAW_GRAPHS) {
+ return;
+ }
+ XYChart chart = new XYChartBuilder()
+ .title(getClass().getSimpleName() + " - " + testName)
+ .width(2200)
+ .height(800)
+ .xAxisTitle("time")
+ .yAxisTitle("concurrency limit")
+ .build();
+ chart.getStyler().setLegendPosition(Styler.LegendPosition.OutsideS);
+ chart.getStyler().setDefaultSeriesRenderStyle(XYSeries.XYSeriesRenderStyle.Line);
+ //noinspection SuspiciousNameCombination
+ chart.getStyler().setYAxisLabelAlignment(AxesChartStyler.TextAlignment.Right);
+ chart.getStyler().setPlotMargin(0);
+ chart.getStyler().setPlotContentSize(.95);
+ drawAnnotationText(chart, "RATE_THRESHOLD",
+ (int) bulkhead.getBulkheadConfig().getFailureRateThreshold());
+ drawAnnotationText(chart, "MAX_CONCURRENT_CALLS",
+ bulkhead.getBulkheadConfig().getMaxConcurrentCalls());
+ drawAnnotationText(chart, "MIN_CONCURRENT_CALLS",
+ bulkhead.getBulkheadConfig().getMinConcurrentCalls());
+ drawAnnotationText(chart, "INITIAL_CONCURRENT_CALLS",
+ bulkhead.getBulkheadConfig().getInitialConcurrentCalls());
+
+ drawAreaSeries(chart, "slowCallsRate", slowCallsRateData, Color.LIGHT_GRAY);
+ drawAreaSeries(chart, "failureCallsRate", errorCallsRateData, Color.GRAY);
+ chart.addSeries("concurrency limit", time, concurrencyLimitData)
+ .setMarkerColor(Color.CYAN)
+ .setLineColor(Color.BLUE);
+ try {
+ BitmapEncoder.saveJPGWithQuality(chart, "./" + testName + ".jpg", 0.95f);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void drawAnnotationText(XYChart chart, String text, int y) {
+ chart.addAnnotation(new AnnotationText(text, 5, y, false));
+ }
+
+ private void drawAreaSeries(XYChart chart, String text, List yData, Color color) {
+ chart.addSeries(text, time, yData)
+ .setXYSeriesRenderStyle(XYSeries.XYSeriesRenderStyle.Area)
+ .setMarker(new None())
+ .setLineColor(color)
+ .setFillColor(color);
+ }
+}
\ No newline at end of file
diff --git a/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadStateMachineTest.java b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadStateMachineTest.java
new file mode 100644
index 0000000000..9367ee2402
--- /dev/null
+++ b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/AdaptiveBulkheadStateMachineTest.java
@@ -0,0 +1,116 @@
+package io.github.resilience4j.bulkhead.adaptive.internal;
+
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkheadConfig;
+import io.github.resilience4j.bulkhead.event.BulkheadOnLimitDecreasedEvent;
+import io.github.resilience4j.bulkhead.event.BulkheadOnLimitIncreasedEvent;
+import io.github.resilience4j.bulkhead.event.BulkheadOnStateTransitionEvent;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AdaptiveBulkheadStateMachineTest {
+
+ private static final int SLOW_CALL_DURATION_THRESHOLD = 200;
+ public static final int RATE_THRESHOLD = 50;
+
+ private AdaptiveBulkheadStateMachine bulkhead;
+ private final List limitIncreases = new LinkedList<>();
+ private final List limitDecreases = new LinkedList<>();
+ private final List stateTransitions = new LinkedList<>();
+
+ @Before
+ public void setup() {
+ AdaptiveBulkheadConfig config = AdaptiveBulkheadConfig.custom()
+ .maxConcurrentCalls(100)
+ .minConcurrentCalls(2)
+ .initialConcurrentCalls(12)
+ .slidingWindowSize(5)
+ .slidingWindowType(AdaptiveBulkheadConfig.SlidingWindowType.TIME_BASED)
+ .minimumNumberOfCalls(3)
+ .failureRateThreshold(RATE_THRESHOLD)
+ .slowCallRateThreshold(RATE_THRESHOLD)
+ .slowCallDurationThreshold(Duration.ofMillis(SLOW_CALL_DURATION_THRESHOLD))
+ .build();
+ bulkhead = (AdaptiveBulkheadStateMachine) AdaptiveBulkhead.of("test", config);
+ bulkhead.getEventPublisher().onLimitIncreased(limitIncreases::add);
+ bulkhead.getEventPublisher().onLimitDecreased(limitDecreases::add);
+ bulkhead.getEventPublisher().onStateTransition(stateTransitions::add);
+ }
+
+ @Test
+ public void testTransitionToCongestionAvoidance() {
+ Throwable failure = new Throwable();
+
+ for (int i = 0; i < 10; i++) {
+ onSuccess();
+ }
+ for (int i = 0; i < 10; i++) {
+ onError(failure);
+ }
+
+ assertThat(limitIncreases)
+ .extracting(BulkheadOnLimitIncreasedEvent::getNewMaxConcurrentCalls)
+ .containsExactly(24, 48, 96);
+ assertThat(limitDecreases)
+ .extracting(BulkheadOnLimitDecreasedEvent::getNewMaxConcurrentCalls)
+ .containsExactly(48, 24, 12);
+ assertThat(stateTransitions)
+ .extracting(BulkheadOnStateTransitionEvent::getToState)
+ .containsExactly(AdaptiveBulkhead.State.CONGESTION_AVOIDANCE);
+ }
+
+ @Test
+ public void testCongestionAvoidanceBelowThresholds() {
+ bulkhead.transitionToCongestionAvoidance();
+
+ for (int i = 0; i < 10; i++) {
+ onSuccess();
+ }
+
+ assertThat(limitDecreases)
+ .isEmpty();
+ assertThat(limitIncreases)
+ .extracting(BulkheadOnLimitIncreasedEvent::getNewMaxConcurrentCalls)
+ .containsExactly(13, 14, 15);
+ assertThat(stateTransitions)
+ .extracting(BulkheadOnStateTransitionEvent::getToState)
+ .containsExactly(AdaptiveBulkhead.State.CONGESTION_AVOIDANCE);
+ assertThat(bulkhead.getMetrics().getFailureRate()).isEqualTo(-1f);
+ }
+
+ @Test
+ public void testCongestionAvoidanceAboveThresholds() {
+ Throwable failure = new Throwable();
+ bulkhead.transitionToCongestionAvoidance();
+
+ for (int i = 0; i < 10; i++) {
+ onError(failure);
+ }
+
+ assertThat(limitIncreases)
+ .isEmpty();
+ assertThat(limitDecreases)
+ .extracting(BulkheadOnLimitDecreasedEvent::getNewMaxConcurrentCalls)
+ .containsExactly(6, 3, 2);
+ assertThat(stateTransitions)
+ .extracting(BulkheadOnStateTransitionEvent::getToState)
+ .containsExactly(AdaptiveBulkhead.State.CONGESTION_AVOIDANCE);
+ assertThat(bulkhead.getMetrics().getFailureRate()).isEqualTo(-1f);
+ }
+
+ private void onSuccess() {
+ bulkhead.onSuccess(1, TimeUnit.MILLISECONDS);
+ }
+
+ private void onError(Throwable failure) {
+ bulkhead.onError(System.currentTimeMillis(), TimeUnit.MILLISECONDS, failure);
+ }
+
+}
\ No newline at end of file
diff --git a/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/InMemoryAdaptiveBulkheadRegistryTest.java b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/InMemoryAdaptiveBulkheadRegistryTest.java
new file mode 100644
index 0000000000..5e1f496aed
--- /dev/null
+++ b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/adaptive/internal/InMemoryAdaptiveBulkheadRegistryTest.java
@@ -0,0 +1,102 @@
+package io.github.resilience4j.bulkhead.adaptive.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkheadConfig;
+import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkheadRegistry;
+
+/**
+ * @author romeh
+ */
+public class InMemoryAdaptiveBulkheadRegistryTest {
+
+ private AdaptiveBulkheadConfig config;
+ private AdaptiveBulkheadRegistry registry;
+
+ @Before
+ public void setUp() {
+ // registry with default config
+ registry = AdaptiveBulkheadRegistry.ofDefaults();
+ // registry with custom config
+ config = AdaptiveBulkheadConfig.custom()
+ .maxConcurrentCalls(300)
+ .slowCallDurationThreshold(Duration.ofMillis(1))
+ .build();
+ }
+
+ @Test
+ public void shouldReturnCustomConfig() {
+ // give
+ AdaptiveBulkheadRegistry registry = AdaptiveBulkheadRegistry.of(config);
+ // when
+ AdaptiveBulkheadConfig bulkheadConfig = registry.getDefaultConfig();
+ // then
+ assertThat(bulkheadConfig).isSameAs(config);
+ }
+
+ @Test
+ public void shouldReturnTheCorrectName() {
+ AdaptiveBulkhead bulkhead = registry.bulkhead("test");
+ assertThat(bulkhead).isNotNull();
+ assertThat(bulkhead.getName()).isEqualTo("test");
+ }
+
+ @Test
+ public void shouldBeTheSameInstance() {
+ AdaptiveBulkhead bulkhead1 = registry.bulkhead("test", config);
+ AdaptiveBulkhead bulkhead2 = registry.bulkhead("test", config);
+
+ assertThat(bulkhead1).isSameAs(bulkhead2);
+ assertThat(registry.getAllBulkheads()).hasSize(1);
+ }
+
+ @Test
+ public void shouldBeNotTheSameInstance() {
+ AdaptiveBulkhead bulkhead1 = registry.bulkhead("test1");
+ AdaptiveBulkhead bulkhead2 = registry.bulkhead("test2");
+
+ assertThat(bulkhead1).isNotSameAs(bulkhead2);
+ assertThat(registry.getAllBulkheads()).hasSize(2);
+ }
+
+ @Test
+ public void testCreateWithConfigurationMap() {
+ Map configs = new HashMap<>();
+ configs.put("default", AdaptiveBulkheadConfig.ofDefaults());
+ configs.put("custom", AdaptiveBulkheadConfig.ofDefaults());
+
+ AdaptiveBulkheadRegistry bulkheadRegistry = AdaptiveBulkheadRegistry.of(configs);
+
+ assertThat(bulkheadRegistry.getDefaultConfig()).isNotNull();
+ assertThat(bulkheadRegistry.getConfiguration("custom")).isNotNull();
+ }
+
+ @Test
+ public void testCreateWithConfigurationMapWithoutDefaultConfig() {
+ Map configs = new HashMap<>();
+ configs.put("custom", AdaptiveBulkheadConfig.ofDefaults());
+
+ AdaptiveBulkheadRegistry bulkheadRegistry = AdaptiveBulkheadRegistry.of(configs);
+
+ assertThat(bulkheadRegistry.getDefaultConfig()).isNotNull();
+ assertThat(bulkheadRegistry.getConfiguration("custom")).isNotNull();
+ }
+
+ @Test
+ public void testAddConfiguration() {
+ AdaptiveBulkheadRegistry bulkheadRegistry = AdaptiveBulkheadRegistry.ofDefaults();
+ bulkheadRegistry.addConfiguration("custom", AdaptiveBulkheadConfig.ofDefaults());
+
+ assertThat(bulkheadRegistry.getDefaultConfig()).isNotNull();
+ assertThat(bulkheadRegistry.getConfiguration("custom")).isNotNull();
+ }
+
+}
\ No newline at end of file
diff --git a/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/internal/AdaptiveBulkheadTest.java b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/internal/AdaptiveBulkheadTest.java
new file mode 100644
index 0000000000..1a3e2c7b5f
--- /dev/null
+++ b/resilience4j-bulkhead/src/test/java/io/github/resilience4j/bulkhead/internal/AdaptiveBulkheadTest.java
@@ -0,0 +1,5 @@
+package io.github.resilience4j.bulkhead.internal;
+
+public class AdaptiveBulkheadTest {
+
+}
diff --git a/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerStateMachineTest.java b/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerStateMachineTest.java
index 31c8090fbe..400e5d05b3 100644
--- a/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerStateMachineTest.java
+++ b/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerStateMachineTest.java
@@ -158,6 +158,38 @@ public void shouldOnlyPermitFourCallsInHalfOpenState() {
}
+ @Test
+ public void shouldOpenAfterFailureRateThresholdExceeded2() {
+ circuitBreaker.onSuccess(0, TimeUnit.NANOSECONDS);
+
+ mockClock.advanceBySeconds(1);
+
+ circuitBreaker.onSuccess(0, TimeUnit.NANOSECONDS);
+
+ mockClock.advanceBySeconds(1);
+
+ circuitBreaker.onSuccess(0, TimeUnit.NANOSECONDS);
+
+ mockClock.advanceBySeconds(1);
+
+ circuitBreaker.onError(0, TimeUnit.NANOSECONDS, new RuntimeException());
+
+ mockClock.advanceBySeconds(1);
+
+ circuitBreaker.onError(0, TimeUnit.NANOSECONDS, new RuntimeException());
+
+ assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
+
+ mockClock.advanceBySeconds(1);
+
+ assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
+
+ circuitBreaker.onError(0, TimeUnit.NANOSECONDS, new RuntimeException());
+
+ assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
+ }
+
+
@Test
public void shouldOpenAfterFailureRateThresholdExceeded2() {
circuitBreaker.onSuccess(0, TimeUnit.NANOSECONDS);
diff --git a/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/AbstractAggregation.java b/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/AbstractAggregation.java
index 579ee8fd6d..e1303b6848 100644
--- a/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/AbstractAggregation.java
+++ b/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/AbstractAggregation.java
@@ -20,7 +20,7 @@
import java.util.concurrent.TimeUnit;
-class AbstractAggregation {
+abstract class AbstractAggregation {
long totalDurationInMillis = 0;
int numberOfSlowCalls = 0;
diff --git a/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/FixedSizeSlidingWindowMetrics.java b/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/FixedSizeSlidingWindowMetrics.java
index 567d6376d4..3353119ad0 100644
--- a/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/FixedSizeSlidingWindowMetrics.java
+++ b/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/FixedSizeSlidingWindowMetrics.java
@@ -41,7 +41,7 @@ public class FixedSizeSlidingWindowMetrics implements Metrics {
private final int windowSize;
private final TotalAggregation totalAggregation;
private final Measurement[] measurements;
- int headIndex;
+ private int headIndex;
/**
* Creates a new {@link FixedSizeSlidingWindowMetrics} with the given window size.
@@ -65,6 +65,15 @@ public synchronized Snapshot record(long duration, TimeUnit durationUnit, Outcom
return new SnapshotImpl(totalAggregation);
}
+ @Override
+ public void resetRecords() {
+ headIndex = 0;
+ totalAggregation.reset();
+ for (Measurement measurement : measurements) {
+ measurement.reset();
+ }
+ }
+
public synchronized Snapshot getSnapshot() {
return new SnapshotImpl(totalAggregation);
}
@@ -92,4 +101,8 @@ private Measurement getLatestMeasurement() {
void moveHeadIndexByOne() {
this.headIndex = (headIndex + 1) % windowSize;
}
+
+ int getHeadIndex() {
+ return headIndex;
+ }
}
\ No newline at end of file
diff --git a/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/Metrics.java b/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/Metrics.java
index 79baa4159f..017d2ed641 100644
--- a/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/Metrics.java
+++ b/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/Metrics.java
@@ -38,8 +38,18 @@ public interface Metrics {
*/
Snapshot getSnapshot();
+ void resetRecords();
+
enum Outcome {
- SUCCESS, ERROR, SLOW_SUCCESS, SLOW_ERROR
+ SUCCESS, ERROR, SLOW_SUCCESS, SLOW_ERROR;
+
+ public static Outcome of(boolean slow, boolean success) {
+ if (success) {
+ return slow ? Outcome.SLOW_SUCCESS : Outcome.SUCCESS;
+ } else {
+ return slow ? Outcome.SLOW_ERROR : Outcome.ERROR;
+ }
+ }
}
}
\ No newline at end of file
diff --git a/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/SlidingTimeWindowMetrics.java b/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/SlidingTimeWindowMetrics.java
index 46d40d0da3..207e7797f3 100644
--- a/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/SlidingTimeWindowMetrics.java
+++ b/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/SlidingTimeWindowMetrics.java
@@ -45,11 +45,11 @@
*/
public class SlidingTimeWindowMetrics implements Metrics {
- final PartialAggregation[] partialAggregations;
+ private final PartialAggregation[] partialAggregations;
private final int timeWindowSizeInSeconds;
private final TotalAggregation totalAggregation;
private final Clock clock;
- int headIndex;
+ private int headIndex;
/**
* Creates a new {@link SlidingTimeWindowMetrics} with the given clock and window of time.
@@ -78,6 +78,12 @@ public synchronized Snapshot record(long duration, TimeUnit durationUnit, Outcom
return new SnapshotImpl(totalAggregation);
}
+ @Override
+ public void resetRecords() {
+ headIndex = 0;
+ totalAggregation.reset();
+ }
+
public synchronized Snapshot getSnapshot() {
moveWindowToCurrentEpochSecond(getLatestPartialAggregation());
return new SnapshotImpl(totalAggregation);
@@ -126,4 +132,12 @@ private PartialAggregation getLatestPartialAggregation() {
void moveHeadIndexByOne() {
this.headIndex = (headIndex + 1) % timeWindowSizeInSeconds;
}
+
+ PartialAggregation[] getPartialAggregations() {
+ return partialAggregations;
+ }
+
+ int getHeadIndex() {
+ return headIndex;
+ }
}
\ No newline at end of file
diff --git a/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/TotalAggregation.java b/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/TotalAggregation.java
index 11649f19ca..a5b0978e5e 100644
--- a/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/TotalAggregation.java
+++ b/resilience4j-core/src/main/java/io/github/resilience4j/core/metrics/TotalAggregation.java
@@ -27,4 +27,12 @@ void removeBucket(AbstractAggregation bucket) {
this.numberOfFailedCalls -= bucket.numberOfFailedCalls;
this.numberOfCalls -= bucket.numberOfCalls;
}
+
+ public void reset() {
+ this.totalDurationInMillis = 0;
+ this.numberOfSlowCalls = 0;
+ this.numberOfSlowFailedCalls = 0;
+ this.numberOfFailedCalls = 0;
+ this.numberOfCalls = 0;
+ }
}
diff --git a/resilience4j-core/src/test/java/io/github/resilience4j/core/metrics/FixedSizeSlidingWindowMetricsTest.java b/resilience4j-core/src/test/java/io/github/resilience4j/core/metrics/FixedSizeSlidingWindowMetricsTest.java
index 704bacb8a7..18fe7a9724 100644
--- a/resilience4j-core/src/test/java/io/github/resilience4j/core/metrics/FixedSizeSlidingWindowMetricsTest.java
+++ b/resilience4j-core/src/test/java/io/github/resilience4j/core/metrics/FixedSizeSlidingWindowMetricsTest.java
@@ -116,25 +116,19 @@ public void testSlowCallsPercentage() {
@Test
public void testMoveHeadIndexByOne() {
FixedSizeSlidingWindowMetrics metrics = new FixedSizeSlidingWindowMetrics(3);
-
- assertThat(metrics.headIndex).isZero();
+ assertThat(metrics.getHeadIndex()).isZero();
metrics.moveHeadIndexByOne();
-
- assertThat(metrics.headIndex).isEqualTo(1);
+ assertThat(metrics.getHeadIndex()).isEqualTo(1);
metrics.moveHeadIndexByOne();
-
- assertThat(metrics.headIndex).isEqualTo(2);
+ assertThat(metrics.getHeadIndex()).isEqualTo(2);
metrics.moveHeadIndexByOne();
-
- assertThat(metrics.headIndex).isZero();
+ assertThat(metrics.getHeadIndex()).isZero();
metrics.moveHeadIndexByOne();
-
- assertThat(metrics.headIndex).isEqualTo(1);
-
+ assertThat(metrics.getHeadIndex()).isEqualTo(1);
}
@Test
diff --git a/resilience4j-core/src/test/java/io/github/resilience4j/core/metrics/SlidingTimeWindowMetricsTest.java b/resilience4j-core/src/test/java/io/github/resilience4j/core/metrics/SlidingTimeWindowMetricsTest.java
index acd493bef8..0c1b76a92d 100644
--- a/resilience4j-core/src/test/java/io/github/resilience4j/core/metrics/SlidingTimeWindowMetricsTest.java
+++ b/resilience4j-core/src/test/java/io/github/resilience4j/core/metrics/SlidingTimeWindowMetricsTest.java
@@ -33,7 +33,7 @@ public void checkInitialBucketCreation() {
MockClock clock = MockClock.at(2019, 8, 4, 12, 0, 0, ZoneId.of("UTC"));
SlidingTimeWindowMetrics metrics = new SlidingTimeWindowMetrics(5, clock);
- PartialAggregation[] buckets = metrics.partialAggregations;
+ PartialAggregation[] buckets = metrics.getPartialAggregations();
long epochSecond = clock.instant().getEpochSecond();
for (int i = 0; i < buckets.length; i++) {
@@ -133,25 +133,19 @@ public void testSlowCallsPercentage() {
public void testMoveHeadIndexByOne() {
MockClock clock = MockClock.at(2019, 8, 4, 12, 0, 0, ZoneId.of("UTC"));
SlidingTimeWindowMetrics metrics = new SlidingTimeWindowMetrics(3, clock);
-
- assertThat(metrics.headIndex).isZero();
+ assertThat(metrics.getHeadIndex()).isZero();
metrics.moveHeadIndexByOne();
-
- assertThat(metrics.headIndex).isEqualTo(1);
+ assertThat(metrics.getHeadIndex()).isEqualTo(1);
metrics.moveHeadIndexByOne();
-
- assertThat(metrics.headIndex).isEqualTo(2);
+ assertThat(metrics.getHeadIndex()).isEqualTo(2);
metrics.moveHeadIndexByOne();
-
- assertThat(metrics.headIndex).isZero();
+ assertThat(metrics.getHeadIndex()).isZero();
metrics.moveHeadIndexByOne();
-
- assertThat(metrics.headIndex).isEqualTo(1);
-
+ assertThat(metrics.getHeadIndex()).isEqualTo(1);
}
@Test
diff --git a/resilience4j-kotlin/src/test/kotlin/io/github/resilience4j/kotlin/circuitbreaker/CoroutineCircuitBreakerTest.kt b/resilience4j-kotlin/src/test/kotlin/io/github/resilience4j/kotlin/circuitbreaker/CoroutineCircuitBreakerTest.kt
index 42d2173bfc..b02a9a360d 100755
--- a/resilience4j-kotlin/src/test/kotlin/io/github/resilience4j/kotlin/circuitbreaker/CoroutineCircuitBreakerTest.kt
+++ b/resilience4j-kotlin/src/test/kotlin/io/github/resilience4j/kotlin/circuitbreaker/CoroutineCircuitBreakerTest.kt
@@ -178,7 +178,7 @@ class CoroutineCircuitBreakerTest {
assertThat(cancellationException!!.message).isEqualTo("test cancel")
assertThat(metrics.numberOfBufferedCalls).isEqualTo(expectedNumberOfFailedCalls)
assertThat(metrics.numberOfFailedCalls).isEqualTo(expectedNumberOfFailedCalls)
- assertThat(metrics.numberOfSuccessfulCalls).isEqualTo(0)
+ assertThat(metrics.numberOfSuccessfulCalls).isZero()
// Then the helloWorldService should be invoked 1 time
assertThat(helloWorldService.invocationCounter).isEqualTo(1)
}
diff --git a/resilience4j-spring/build.gradle b/resilience4j-spring/build.gradle
index 91659b724e..be41aa5f31 100644
--- a/resilience4j-spring/build.gradle
+++ b/resilience4j-spring/build.gradle
@@ -2,7 +2,6 @@ dependencies {
api project(':resilience4j-annotations')
api project(':resilience4j-consumer')
api project(':resilience4j-framework-common')
-
compileOnly(libraries.aspectj)
compileOnly(libraries.hibernate_validator)
compileOnly(libraries.spring_core,libraries.spring_context)
diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspect.java b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspect.java
index 077c8939a5..b903872d88 100644
--- a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspect.java
+++ b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspect.java
@@ -15,6 +15,7 @@
*/
package io.github.resilience4j.bulkhead.configure;
+import io.github.resilience4j.bulkhead.BulkheadFullException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
@@ -23,7 +24,6 @@
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
-
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;