diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 79ee123c2b..b784ffe676 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/build.gradle b/build.gradle index b4ba00eb68..005a18feb6 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ allprojects { version = '2.1.0-SNAPSHOT' group = 'io.github.resilience4j' - description = 'Resilience4j is a lightweight, easy-to-use fault tolerance library designed for Java8 and functional programming' + description = 'Resilience4j is a lightweight, easy-to-use fault tolerance library designed for Java17+ and functional programming' repositories { mavenCentral() diff --git a/resilience4j-bulkhead/build.gradle b/resilience4j-bulkhead/build.gradle index f46263e132..5193f3607f 100644 --- a/resilience4j-bulkhead/build.gradle +++ b/resilience4j-bulkhead/build.gradle @@ -1,5 +1,6 @@ dependencies { api project(':resilience4j-core') testImplementation project(':resilience4j-test') + testImplementation 'org.knowm.xchart:xchart:3.8.3' } ext.moduleName = 'io.github.resilience4j.bulkhead' \ No newline at end of file diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/BulkheadAdaptationConfig.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/BulkheadAdaptationConfig.java new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/BulkheadFullException.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/BulkheadFullException.java index 07ca05f5e3..9db4ce49c6 100644 --- a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/BulkheadFullException.java +++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/BulkheadFullException.java @@ -18,11 +18,16 @@ */ package io.github.resilience4j.bulkhead; +import io.github.resilience4j.bulkhead.adaptive.AdaptiveBulkhead; + /** * A {@link BulkheadFullException} signals that the bulkhead is full. */ public class BulkheadFullException extends RuntimeException { + private static final String ERROR_FURTHER_CALLS = "Bulkhead '%s' is full and does not permit further calls"; + private static final String ERROR_PERMISSION_WAIT = "Bulkhead '%s' is full and thread was interrupted during permission wait"; + private BulkheadFullException(String message, boolean writableStackTrace) { super(message, null, false, writableStackTrace); } @@ -33,17 +38,14 @@ private BulkheadFullException(String message, boolean writableStackTrace) { * @param bulkhead the Bulkhead. */ public static BulkheadFullException createBulkheadFullException(Bulkhead bulkhead) { - boolean writableStackTraceEnabled = bulkhead.getBulkheadConfig() + boolean writableStackTraceEnabled = bulkhead.getBulkheadConfig() .isWritableStackTraceEnabled(); String message; if (Thread.currentThread().isInterrupted()) { - message = String - .format("Bulkhead '%s' is full and thread was interrupted during permission wait", - bulkhead.getName()); + message = String.format(ERROR_PERMISSION_WAIT, bulkhead.getName()); } else { - message = String.format("Bulkhead '%s' is full and does not permit further calls", - bulkhead.getName()); + message = String.format(ERROR_FURTHER_CALLS, bulkhead.getName()); } return new BulkheadFullException(message, writableStackTraceEnabled); @@ -58,8 +60,28 @@ public static BulkheadFullException createBulkheadFullException(ThreadPoolBulkhe boolean writableStackTraceEnabled = bulkhead.getBulkheadConfig() .isWritableStackTraceEnabled(); - String message = String - .format("Bulkhead '%s' is full and does not permit further calls", bulkhead.getName()); + String message = String.format(ERROR_FURTHER_CALLS, bulkhead.getName()); + + return new BulkheadFullException(message, writableStackTraceEnabled); + } + /** + * The constructor with a message. + * + * @param bulkhead the AdaptiveLimitBulkhead. + */ + public BulkheadFullException(AdaptiveBulkhead bulkhead) { + super(String.format(ERROR_FURTHER_CALLS, bulkhead.getName())); + } + + /** + * The constructor with a message. + * + * @param bulkhead the AdaptiveBulkheadStateMachine. + */ + public static BulkheadFullException createBulkheadFullException(AdaptiveBulkhead bulkhead) { + boolean writableStackTraceEnabled = bulkhead.getBulkheadConfig().isWritableStackTraceEnabled(); + + String message = String.format(ERROR_FURTHER_CALLS, bulkhead.getName()); return new BulkheadFullException(message, writableStackTraceEnabled); } diff --git a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/ThreadPoolBulkhead.java b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/ThreadPoolBulkhead.java index e421c97887..362c86a79e 100644 --- a/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/ThreadPoolBulkhead.java +++ b/resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/ThreadPoolBulkhead.java @@ -186,21 +186,6 @@ static ThreadPoolBulkhead of(String name, */ ThreadPoolBulkheadEventPublisher getEventPublisher(); - /** - * Returns a supplier which submits a value-returning task for execution and - * returns a CompletionStage representing the asynchronous computation of the task. - * - * The Supplier throws a {@link BulkheadFullException} if the task cannot be submitted, because the Bulkhead is full. - * - * @param supplier the value-returning task to submit - * @param the result type of the callable - * @return a supplier which submits a value-returning task for execution and returns a CompletionStage representing - * the asynchronous computation of the task - */ - default Supplier> decorateSupplier(Supplier 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[] recordExceptions = new Class[0]; + @SuppressWarnings("unchecked") + private Class[] 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[] recordExceptions = new Class[0]; + @SuppressWarnings("unchecked") + private Class[] 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... 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... 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 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 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 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 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;