I am having a hard time to cancel/interrupt a CompletableFuture task, so it times out on the controller, but it doest stop the execution of the client even it takes more time
What I am missing? to be able to cancel my client execution on timeout
I have the next service:
@TimeLimiter(name = "ly-service-timelimiter", fallbackMethod = "fallFn")
@Bulkhead(name = "ly-service-bulkhead", fallbackMethod = "fallFn", type = Bulkhead.Type.THREADPOOL)
@Override
public CompletableFuture<Void> myMethod(Request request) throws Exception {
try {
log.info("MyMethod Service: {}", request);
return client.myMethod(request);
} catch (RuntimeException e) {
log.info("Exception", request);
throw new RuntimeException(e);
}
}
The client
public CompletableFuture<Void> myMethod(Request request) {
CompletableFuture<Void> future = new CompletableFuture<>();
CompletableFuture.runAsync(() -> {
if (future.isCancelled()) {
log.info("MyMethod was cancelled before execution.");
return;
}
try {
log.info("Processing request", request);
ThreadUtil.fakeRandomSleep(10000); // Simulating work
if (future.isCancelled()) {
log.info("Processing was cancelled during execution.");
} else {
log.info("Completed routing with TimeOut");
future.complete(null);
}
} catch (Exception e) {
log.info("completeExceptionally......");
future.completeExceptionally(e);
}
});
return future;
}
The controller:
@GetMapping("/runTimeOut")
public @ResponseBody String executeSample() throws ExecutionException, InterruptedException {
log.info("Execute TimeOut EndPoint");
Request request = Request.builder().build(); //Any class
try {
myService.myMethod(request).get();
}catch (ExecutionException ex){
if(ex.getCause() instanceof java.util.concurrent.TimeoutException){
log.info("TimeoutException occurred");
}
return "Failed";
}
return "OK";
}
My config is:
resilience4j:
bulkhead:
configs:
default:
max-concurrent-calls: 2
max-wait-duration: 0ms
instances:
ly-service-bulkhead:
base-config: default
timelimiter:
instances:
ly-service-timelimiter:
timeoutDuration: 900ms
cancel-running-future: true
My libraries
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
<relativePath/>
</parent>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>2.1.0</version>
</dependency>
You cannot cancel the running task if using CompletableFuture provided by JDK as it is its limitation. You have to use Future to do it.
But @TimeLimiter does not support on Future right now. That means you cannot use the annotation approach if you want to cancel a task once it is timeout. It is also mentioned in the resilience4j Github issue as follows :
You can use a TimeLimiter to decorate a method which returns a Future. A future can be canceled. But unfortunately Futures are not yet supported via annotations/aspects, but only via the functional style. We tried to implement it, but supporting Future for TimeLimiter, Retry, Bulkhead, CircuitBreaker via annotations caused us some headaches.
So the easiest way is to represent the result of the long running task using Future and timeout it programmatically using its decorators approach instead of the annotation approach.
So first make the client can execute a long running task which its result is returned as Future :
public class Client {
private ExecutorService executors = Executors.newFixedThreadPool(5);
public Future<Void> myMethod() {
return (Future<Void>) executors.submit(() -> runSomeLongRunningTask());
}
private void runSomeLongRunningTask() {
// logic of the task here
// check if the task is cancelled here and throw exception if yes.
}
}
You have to use the Thread.currentThread().isInterrupted() to check if the long running task is cancelled and throw the exception if yes.
Then in the service , the equivalent decorators approach to call the client is likes :
public class Service {
private ExecutorService executor = Executors.newCachedThreadPool();
@Bulkhead(name = "ly-service-bulkhead", fallbackMethod = "fallFn", type = Bulkhead.Type.THREADPOOL)
public Future<Void> myMethod() {
TimeLimiterConfig config = TimeLimiterConfig.custom()
.cancelRunningFuture(true)
.timeoutDuration(Duration.ofMillis(900))
.build();
TimeLimiter timeLimiter = TimeLimiter.of("ly-service-timelimiter", config);
Callable<Void> taskDecorateWithTimeout = () -> {
try {
return timeLimiter.decorateFutureSupplier(() -> client.myMethod()).call();
} catch (TimeoutException ex) {
//manually call the fallback method in case of TimeoutException
fallFn(ex);
throw ex;
}
};
return executor.submit(taskDecorateWithTimeout);
}
}
And the controller should not require any changes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With