Skip to content

Commit 2171ed5

Browse files
authored
Do not create and cache handler for fallbacks. These handlers require access to the current invocation context (for fallback method parameters) and caching them fails when that changes. Handlers are now cached for all FT annotations except fallbacks. New test added to verify the changes. (helidon-io#2546)
Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>
1 parent 552d033 commit 2171ed5

4 files changed

Lines changed: 153 additions & 28 deletions

File tree

microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/MethodInvoker.java

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,22 @@ class MethodInvoker implements FtSupplier<Object> {
161161
* FT handler created for the method.
162162
*/
163163
private static class MethodState {
164-
private FtHandlerTyped<Object> handler;
165164
private Retry retry;
166165
private Bulkhead bulkhead;
167166
private CircuitBreaker breaker;
167+
private Timeout timeout;
168168
private State lastBreakerState;
169169
private long breakerTimerOpen;
170170
private long breakerTimerClosed;
171171
private long breakerTimerHalfOpen;
172172
private long startNanos;
173173
}
174174

175+
/**
176+
* FT handler for this invoker.
177+
*/
178+
private FtHandlerTyped<Object> handler;
179+
175180
/**
176181
* A key used to lookup {@code MethodState} instances, which include FT handlers.
177182
* A class loader is necessary to support multiple applications as seen in the TCKs.
@@ -302,10 +307,13 @@ public boolean cancel(boolean mayInterruptIfRunning) {
302307
methodState.breakerTimerHalfOpen = 0L;
303308
methodState.startNanos = System.nanoTime();
304309
}
305-
methodState.handler = createMethodHandler(methodState);
310+
initMethodHandler(methodState);
306311
return methodState;
307312
});
308313

314+
// Create a new method handler to ensure correct context in fallback
315+
handler = createMethodHandler(methodState);
316+
309317
// Gather information about current request scope if active
310318
try {
311319
requestController = CDI.current().select(RequestContextController.class).get();
@@ -380,7 +388,7 @@ public Object get() throws Throwable {
380388
Supplier<Single<?>> supplier = () -> {
381389
try {
382390
return Contexts.runInContextWithThrow(helidonContext,
383-
() -> methodState.handler.invoke(toCompletionStageSupplier(context::proceed)));
391+
() -> handler.invoke(toCompletionStageSupplier(context::proceed)));
384392
} catch (Exception e) {
385393
return Single.error(e);
386394
}
@@ -496,38 +504,27 @@ private FtSupplier<Object> requestContextSupplier(FtSupplier<Object> supplier) {
496504
}
497505

498506
/**
499-
* Creates a FT handler for an invocation by inspecting annotations. Handlers
500-
* are composed as follows:
501-
*
502-
* fallback(retry(circuitbreaker(timeout(bulkhead(method)))))
503-
*
504-
* Note that timeout includes the time an invocation may be queued in a
505-
* bulkhead, so it needs to be before the bulkhead call.
507+
* Initializes method state by creating handlers for all FT annotations
508+
* except fallbacks. A fallback can reference the current invocation context
509+
* (via fallback method parameters) and cannot be cached.
506510
*
507511
* @param methodState State related to this invocation's method.
508512
*/
509-
private FtHandlerTyped<Object> createMethodHandler(MethodState methodState) {
510-
FaultTolerance.TypedBuilder<Object> builder = FaultTolerance.typedBuilder();
511-
512-
// Create and add bulkhead
513+
private void initMethodHandler(MethodState methodState) {
513514
if (introspector.hasBulkhead()) {
514515
methodState.bulkhead = Bulkhead.builder()
515516
.limit(introspector.getBulkhead().value())
516517
.queueLength(introspector.isAsynchronous() ? introspector.getBulkhead().waitingTaskQueue() : 0)
517518
.build();
518-
builder.addBulkhead(methodState.bulkhead);
519519
}
520520

521-
// Create and add timeout handler
522521
if (introspector.hasTimeout()) {
523-
Timeout timeout = Timeout.builder()
522+
methodState.timeout = Timeout.builder()
524523
.timeout(Duration.of(introspector.getTimeout().value(), introspector.getTimeout().unit()))
525524
.currentThread(!introspector.isAsynchronous())
526525
.build();
527-
builder.addTimeout(timeout);
528526
}
529527

530-
// Create and add circuit breaker
531528
if (introspector.hasCircuitBreaker()) {
532529
methodState.breaker = CircuitBreaker.builder()
533530
.delay(Duration.of(introspector.getCircuitBreaker().delay(),
@@ -538,29 +535,55 @@ private FtHandlerTyped<Object> createMethodHandler(MethodState methodState) {
538535
.applyOn(mapTypes(introspector.getCircuitBreaker().failOn()))
539536
.skipOn(mapTypes(introspector.getCircuitBreaker().skipOn()))
540537
.build();
541-
builder.addBreaker(methodState.breaker);
542538
}
543539

544-
// Create and add retry handler
545540
if (introspector.hasRetry()) {
546-
Retry retry = Retry.builder()
541+
methodState.retry = Retry.builder()
547542
.retryPolicy(Retry.JitterRetryPolicy.builder()
548543
.calls(introspector.getRetry().maxRetries() + 1)
549544
.delay(Duration.of(introspector.getRetry().delay(),
550-
introspector.getRetry().delayUnit()))
545+
introspector.getRetry().delayUnit()))
551546
.jitter(Duration.of(introspector.getRetry().jitter(),
552-
introspector.getRetry().jitterDelayUnit()))
547+
introspector.getRetry().jitterDelayUnit()))
553548
.build())
554549
.overallTimeout(Duration.of(introspector.getRetry().maxDuration(),
555-
introspector.getRetry().durationUnit()))
550+
introspector.getRetry().durationUnit()))
556551
.applyOn(mapTypes(introspector.getRetry().retryOn()))
557552
.skipOn(mapTypes(introspector.getRetry().abortOn()))
558553
.build();
559-
builder.addRetry(retry);
560-
methodState.retry = retry; // keep reference to Retry
554+
}
555+
}
556+
557+
/**
558+
* Creates a FT handler for this invocation. Handlers are composed as follows:
559+
*
560+
* fallback(retry(circuitbreaker(timeout(bulkhead(method)))))
561+
*
562+
* Uses the cached handlers defined in the method state for this invocation's
563+
* method, except for fallback.
564+
*
565+
* @param methodState State related to this invocation's method.
566+
*/
567+
private FtHandlerTyped<Object> createMethodHandler(MethodState methodState) {
568+
FaultTolerance.TypedBuilder<Object> builder = FaultTolerance.typedBuilder();
569+
570+
if (methodState.bulkhead != null) {
571+
builder.addBulkhead(methodState.bulkhead);
572+
}
573+
574+
if (methodState.timeout != null) {
575+
builder.addTimeout(methodState.timeout);
576+
}
577+
578+
if (methodState.breaker != null) {
579+
builder.addBreaker(methodState.breaker);
580+
}
581+
582+
if (methodState.retry != null) {
583+
builder.addRetry(methodState.retry);
561584
}
562585

563-
// Create and add fallback handler
586+
// Create and add fallback handler for this invocation
564587
if (introspector.hasFallback()) {
565588
Fallback<Object> fallback = Fallback.builder()
566589
.fallback(throwable -> {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2020 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.tests.functional.requestscope;
17+
18+
import javax.enterprise.context.RequestScoped;
19+
import javax.inject.Inject;
20+
import javax.ws.rs.GET;
21+
import javax.ws.rs.Path;
22+
import javax.ws.rs.QueryParam;
23+
import javax.ws.rs.WebApplicationException;
24+
import javax.ws.rs.core.Response;
25+
26+
@RequestScoped
27+
@Path("/test3")
28+
public class MyResource {
29+
30+
@Inject
31+
SomeService3 someService3;
32+
33+
@GET
34+
public String getTestResource(@QueryParam("param1") String param1) {
35+
try {
36+
return someService3.test(param1);
37+
} catch (Exception e) {
38+
System.out.println(e.getMessage());
39+
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
40+
}
41+
}
42+
43+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2020 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.tests.functional.requestscope;
17+
18+
import javax.enterprise.context.ApplicationScoped;
19+
import java.util.concurrent.atomic.AtomicLong;
20+
21+
import org.eclipse.microprofile.faulttolerance.Fallback;
22+
23+
@ApplicationScoped
24+
public class SomeService3 {
25+
26+
private AtomicLong counter = new AtomicLong(0);
27+
28+
@Fallback(fallbackMethod = "testFallback")
29+
public String test(String testParam) throws Exception {
30+
maybeFail();
31+
return testParam;
32+
}
33+
34+
public String testFallback(String testParam) throws Exception {
35+
return testParam;
36+
}
37+
38+
private void maybeFail() {
39+
final Long invocationNumber = counter.getAndIncrement();
40+
if (invocationNumber % 4 > 1) { // alternate 2 successful and 2 failing invocations
41+
throw new RuntimeException("Service failed.");
42+
}
43+
}
44+
}

tests/functional/request-scope/src/test/java/io/helidon/tests/functional/requestscope/TenantTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,19 @@ public void test2() {
5151
assertThat(r.getStatus(), is(HttpResponseStatus.OK.code()));
5252
}
5353
}
54+
55+
@Test
56+
public void test3() {
57+
Response r;
58+
for (int i = 0; i < 3; i++) {
59+
String paramValue = Integer.toString(i);
60+
r = baseTarget.path("test3")
61+
.queryParam("param1", paramValue)
62+
.request()
63+
.get();
64+
assertThat(r.getStatus(), is(HttpResponseStatus.OK.code()));
65+
String entityValue = r.readEntity(String.class);
66+
assertThat(entityValue, is(paramValue));
67+
}
68+
}
5469
}

0 commit comments

Comments
 (0)