Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resilience4j and @timeLimiter annotation for timing out and cancel Thread - CompletableFuture

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>

like image 879
JPRLCol Avatar asked Oct 23 '25 16:10

JPRLCol


1 Answers

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.

like image 69
Ken Chan Avatar answered Oct 26 '25 05:10

Ken Chan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!