Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring @Cacheable and @Async annotation

I have the need to cache some the results of some asynchronous computations. In detail, to overcome this issue, I am trying to use Spring 4.3 cache and asynchronous computation features.

As an example, let's take the following code:

@Service
class AsyncService {
    @Async
    @Cacheable("users")
    CompletableFuture<User> findById(String usedId) {
        // Some code that retrieves the user relative to id userId
        return CompletableFuture.completedFuture(user);
    }
}

Is it possible? I mean, will the caching abstraction of Spring handle correctly the objects of type CompletableFuture<User>? I know that Caffeine Cache has something like that, but I can't understand if Spring uses it if properly configured.

EDIT: I am not interested in the User object itself, but in the CompletableFuture that represents the computation.

like image 317
riccardo.cardin Avatar asked Nov 07 '17 14:11

riccardo.cardin


2 Answers

The community asks me to do some experiments, so I made them. I found that the answer to my question is simple: @Cacheable and @Async do not work together if they are placed above the same method.

To be clear, I was not asking for a way to directly make the cache return the object owned by a CompletableFuture. This is impossible, and if it isn't so, it will break the contract of asynchronous computation of the CompletableFuture class.

As I said, the two annotations do not work together on the same method. If you think about it, it is obvious. Marking with @Async is also @Cacheable means to delegate the whole cache management to different asynchronous threads. If the computation of the value of the CompletableFuture will take a long time to complete, the value in the cache will be placed after that time by Spring Proxy.

Obviously, there is a workaround. The workaround uses the fact the CompletableFuture is a promise. Let's have a look at the code below.

@Component
public class CachedService {
    /* Dependecies resolution code */
    private final AsyncService service;

    @Cacheable(cacheNames = "ints")
    public CompletableFuture<Integer> randomIntUsingSpringAsync() throws InterruptedException {
        final CompletableFuture<Integer> promise = new CompletableFuture<>();
        // Letting an asynchronous method to complete the promise in the future
        service.performTask(promise);
        // Returning the promise immediately
        return promise;
    }
}

@Component
public class AsyncService {
    @Async
    void performTask(CompletableFuture<Integer> promise) throws InterruptedException {
        Thread.sleep(2000);
        // Completing the promise asynchronously
        promise.complete(random.nextInt(1000));
    }
}

The trick is to create an incomplete promise and return it immediately from the method marked with the @Cacheable annotation. The promise will be completed asynchronously by another bean that owns the method marked with the @Async annotation.

As a bonus, I also implemented a solution that does not use the Spring @Async annotation, but it uses the factory methods available in the CompletableFuture class directly.

@Cacheable(cacheNames = "ints1")
public CompletableFuture<Integer> randomIntNativelyAsync() throws
        InterruptedException {
    return CompletableFuture.supplyAsync(this::getAsyncInteger, executor);
}

private Integer getAsyncInteger() {
    logger.info("Entering performTask");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return random.nextInt(1000);
}

Anyway, I shared the complete solution to my GitHub problem, spring-cacheable-async.

Finally, the above is a long description of what the Jira SPR-12967 refers to.

I hope it helps. Cheers.

like image 199
riccardo.cardin Avatar answered Sep 20 '22 14:09

riccardo.cardin


As per SPR-12967, ListenableFuture (CompletableFuture) are not supported.

like image 35
sol4me Avatar answered Sep 20 '22 14:09

sol4me