Skip to content

Commit 9f62d71

Browse files
pulkitmehraRobWin
authored andcommitted
Issue ReactiveX#657: Added Future decorator to Bulkhead interface (ReactiveX#729)
1 parent e0221c7 commit 9f62d71

File tree

2 files changed

+291
-3
lines changed

2 files changed

+291
-3
lines changed

resilience4j-bulkhead/src/main/java/io/github/resilience4j/bulkhead/Bulkhead.java

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead;
2626
import io.github.resilience4j.core.EventConsumer;
2727
import io.github.resilience4j.core.exception.AcquirePermissionCancelledException;
28+
import io.github.resilience4j.core.functions.OnceConsumer;
2829
import io.vavr.CheckedConsumer;
2930
import io.vavr.CheckedFunction0;
3031
import io.vavr.CheckedFunction1;
@@ -33,9 +34,8 @@
3334
import io.vavr.control.Either;
3435
import io.vavr.control.Try;
3536

36-
import java.util.concurrent.Callable;
37-
import java.util.concurrent.CompletableFuture;
38-
import java.util.concurrent.CompletionStage;
37+
import java.util.Objects;
38+
import java.util.concurrent.*;
3939
import java.util.function.Consumer;
4040
import java.util.function.Function;
4141
import java.util.function.Supplier;
@@ -118,6 +118,32 @@ static <T> Supplier<CompletionStage<T>> decorateCompletionStage(Bulkhead bulkhea
118118
};
119119
}
120120

121+
/**
122+
* Returns a supplier of type Future which is decorated by a bulkhead. Bulkhead will reserve permission until {@link Future#get()}
123+
* or {@link Future#get(long, TimeUnit)} is evaluated even if the underlying call took less time to return. Any delays in evaluating
124+
* future will result in holding of permission in the underlying Semaphore.
125+
*
126+
* @param bulkhead the bulkhead
127+
* @param supplier the original supplier
128+
* @param <T> the type of the returned Future result
129+
* @return a supplier which is decorated by a Bulkhead.
130+
*/
131+
static <T> Supplier<Future<T>> decorateFuture(Bulkhead bulkhead, Supplier<Future<T>> supplier) {
132+
return () -> {
133+
if (!bulkhead.tryAcquirePermission()) {
134+
final CompletableFuture<T> promise = new CompletableFuture<>();
135+
promise.completeExceptionally(BulkheadFullException.createBulkheadFullException(bulkhead));
136+
return promise;
137+
}
138+
try {
139+
return new BulkheadFuture<T>(bulkhead, supplier.get());
140+
} catch (Throwable e) {
141+
bulkhead.onComplete();
142+
throw e;
143+
}
144+
};
145+
}
146+
121147
/**
122148
* Returns a runnable which is decorated by a bulkhead.
123149
*
@@ -564,4 +590,54 @@ interface EventPublisher extends io.github.resilience4j.core.EventPublisher<Bulk
564590

565591
EventPublisher onCallFinished(EventConsumer<BulkheadOnCallFinishedEvent> eventConsumer);
566592
}
593+
594+
/**
595+
* This class decorates future with Bulkhead functionality around invocation.
596+
*
597+
* @param <T> of return type
598+
*/
599+
final class BulkheadFuture<T> implements Future<T> {
600+
final private Future<T> future;
601+
final private OnceConsumer<Bulkhead> onceToBulkhead;
602+
603+
BulkheadFuture(Bulkhead bulkhead, Future<T> future) {
604+
Objects.requireNonNull(future, "Non null Future is required to decorate");
605+
this.onceToBulkhead = OnceConsumer.of(bulkhead);
606+
this.future = future;
607+
608+
}
609+
610+
@Override
611+
public boolean cancel(boolean mayInterruptIfRunning) {
612+
return future.cancel(mayInterruptIfRunning);
613+
}
614+
615+
@Override
616+
public boolean isCancelled() {
617+
return future.isCancelled();
618+
}
619+
620+
@Override
621+
public boolean isDone() {
622+
return future.isDone();
623+
}
624+
625+
@Override
626+
public T get() throws InterruptedException, ExecutionException {
627+
try {
628+
return future.get();
629+
} finally {
630+
onceToBulkhead.applyOnce(bh -> bh.onComplete());
631+
}
632+
}
633+
634+
@Override
635+
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
636+
try {
637+
return future.get(timeout, unit);
638+
} finally {
639+
onceToBulkhead.applyOnce(bh -> bh.onComplete());
640+
}
641+
}
642+
}
567643
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package io.github.resilience4j.bulkhead;
2+
3+
import io.github.resilience4j.test.HelloWorldService;
4+
import org.junit.Before;
5+
import org.junit.Test;
6+
7+
import java.util.concurrent.*;
8+
import java.util.function.Supplier;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
import static org.assertj.core.api.Assertions.catchThrowable;
12+
import static org.mockito.ArgumentMatchers.any;
13+
import static org.mockito.ArgumentMatchers.anyLong;
14+
import static org.mockito.BDDMockito.given;
15+
import static org.mockito.BDDMockito.then;
16+
import static org.mockito.Mockito.mock;
17+
import static org.mockito.Mockito.times;
18+
19+
public class BulkheadFutureTest {
20+
21+
private HelloWorldService helloWorldService;
22+
private Future future;
23+
private BulkheadConfig config;
24+
25+
@Before
26+
public void setUp() {
27+
helloWorldService = mock(HelloWorldService.class);
28+
future = mock(Future.class);
29+
config = BulkheadConfig.custom()
30+
.maxConcurrentCalls(1)
31+
.build();
32+
}
33+
34+
@Test
35+
public void shouldDecorateSupplierAndReturnWithSuccess() throws Exception {
36+
Bulkhead bulkhead = Bulkhead.of("test", config);
37+
38+
given(future.get()).willReturn("Hello world");
39+
given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
40+
41+
Supplier<Future<String>> supplier = Bulkhead
42+
.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
43+
44+
String result = supplier.get().get();
45+
46+
assertThat(result).isEqualTo("Hello world");
47+
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
48+
then(helloWorldService).should(times(1)).returnHelloWorldFuture();
49+
then(future).should(times(1)).get();
50+
}
51+
52+
@Test
53+
public void shouldDecorateSupplierAndReturnWithSuccessAndTimeout() throws Exception {
54+
Bulkhead bulkhead = Bulkhead.of("test", config);
55+
56+
given(future.get(anyLong(), any(TimeUnit.class))).willReturn("Hello world");
57+
given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
58+
59+
Supplier<Future<String>> supplier = Bulkhead
60+
.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
61+
62+
String result = supplier.get().get(5, TimeUnit.SECONDS);
63+
64+
assertThat(result).isEqualTo("Hello world");
65+
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
66+
then(helloWorldService).should(times(1)).returnHelloWorldFuture();
67+
then(future).should(times(1)).get(anyLong(), any(TimeUnit.class));
68+
}
69+
70+
@Test
71+
public void shouldDecorateFutureAndBulkheadApplyOnceOnMultipleFutureEval() throws Exception {
72+
Bulkhead bulkhead = Bulkhead.of("test", config);
73+
74+
given(future.get(anyLong(), any(TimeUnit.class))).willReturn("Hello world");
75+
given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
76+
77+
Supplier<Future<String>> supplier = Bulkhead
78+
.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
79+
80+
Future<String> decoratedFuture = supplier.get();
81+
82+
decoratedFuture.get(5, TimeUnit.SECONDS);
83+
decoratedFuture.get(5, TimeUnit.SECONDS);
84+
85+
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
86+
then(helloWorldService).should(times(1)).returnHelloWorldFuture();
87+
then(future).should(times(2)).get(anyLong(), any(TimeUnit.class));
88+
}
89+
90+
@Test
91+
public void shouldDecorateFutureAndBulkheadApplyOnceOnMultipleFutureEvalFailure() throws Exception {
92+
Bulkhead bulkhead = Bulkhead.of("test", config);
93+
94+
given(future.get()).willThrow(new ExecutionException(new RuntimeException("Hello world")));
95+
given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
96+
97+
Supplier<Future<String>> supplier = Bulkhead
98+
.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
99+
100+
Future<String> decoratedFuture = supplier.get();
101+
102+
catchThrowable(() -> decoratedFuture.get());
103+
catchThrowable(() -> decoratedFuture.get());
104+
105+
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
106+
then(helloWorldService).should(times(1)).returnHelloWorldFuture();
107+
then(future).should(times(2)).get();
108+
}
109+
110+
@Test
111+
public void shouldDecorateSupplierAndReturnWithExceptionAtAsyncStage() throws Exception {
112+
Bulkhead bulkhead = Bulkhead.of("test", config);
113+
114+
given(future.get()).willThrow(new ExecutionException(new RuntimeException("BAM!")));
115+
given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
116+
117+
Supplier<Future<String>> supplier = Bulkhead
118+
.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
119+
120+
Throwable thrown = catchThrowable(() -> supplier.get().get());
121+
122+
assertThat(thrown).isInstanceOf(ExecutionException.class)
123+
.hasCauseInstanceOf(RuntimeException.class);
124+
125+
assertThat(thrown.getCause().getMessage()).isEqualTo("BAM!");
126+
127+
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
128+
then(helloWorldService).should(times(1)).returnHelloWorldFuture();
129+
then(future).should(times(1)).get();
130+
}
131+
132+
@Test
133+
public void shouldDecorateSupplierAndReturnWithExceptionAtSyncStage() throws Exception {
134+
Bulkhead bulkhead = Bulkhead.of("test", config);
135+
136+
given(helloWorldService.returnHelloWorldFuture()).willThrow(new RuntimeException("BAM!"));
137+
138+
Supplier<Future<String>> supplier = Bulkhead
139+
.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
140+
141+
Throwable thrown = catchThrowable(() -> supplier.get().get());
142+
143+
assertThat(thrown).isInstanceOf(RuntimeException.class)
144+
.hasMessage("BAM!");
145+
146+
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
147+
then(helloWorldService).should(times(1)).returnHelloWorldFuture();
148+
then(future).shouldHaveZeroInteractions();
149+
}
150+
151+
@Test
152+
public void shouldReturnFailureWithBulkheadFullException() throws Exception {
153+
// tag::bulkheadFullException[]
154+
BulkheadConfig config = BulkheadConfig.custom().maxConcurrentCalls(2).build();
155+
Bulkhead bulkhead = Bulkhead.of("test", config);
156+
bulkhead.tryAcquirePermission();
157+
bulkhead.tryAcquirePermission();
158+
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(0);
159+
160+
given(future.get()).willReturn("Hello world");
161+
given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
162+
163+
Supplier<Future<String>> supplier = Bulkhead.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
164+
165+
Throwable thrown = catchThrowable(() -> supplier.get().get());
166+
167+
assertThat(thrown).isInstanceOf(ExecutionException.class)
168+
.hasCauseInstanceOf(BulkheadFullException.class);
169+
170+
then(helloWorldService).shouldHaveZeroInteractions();
171+
then(future).shouldHaveZeroInteractions();
172+
// end::bulkheadFullException[]
173+
}
174+
175+
@Test
176+
public void shouldReturnFailureWithFutureCancellationException() throws Exception {
177+
Bulkhead bulkhead = Bulkhead.of("test", config);
178+
179+
given(future.get()).willThrow(new CancellationException());
180+
given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
181+
182+
Supplier<Future<String>> supplier = Bulkhead
183+
.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
184+
185+
Throwable thrown = catchThrowable(() -> supplier.get().get());
186+
187+
assertThat(thrown).isInstanceOf(CancellationException.class);
188+
189+
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
190+
then(helloWorldService).should(times(1)).returnHelloWorldFuture();
191+
then(future).should(times(1)).get();
192+
}
193+
194+
@Test
195+
public void shouldReturnFailureWithFutureTimeoutException() throws Exception {
196+
Bulkhead bulkhead = Bulkhead.of("test", config);
197+
198+
given(future.get(anyLong(), any(TimeUnit.class))).willThrow(new TimeoutException());
199+
given(helloWorldService.returnHelloWorldFuture()).willReturn(future);
200+
201+
Supplier<Future<String>> supplier = Bulkhead
202+
.decorateFuture(bulkhead, helloWorldService::returnHelloWorldFuture);
203+
204+
Throwable thrown = catchThrowable(() -> supplier.get().get(5, TimeUnit.SECONDS));
205+
206+
assertThat(thrown).isInstanceOf(TimeoutException.class);
207+
208+
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
209+
then(helloWorldService).should(times(1)).returnHelloWorldFuture();
210+
then(future).should(times(1)).get(anyLong(), any(TimeUnit.class));
211+
}
212+
}

0 commit comments

Comments
 (0)