11

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.

riccardo.cardin
  • 7,971
  • 5
  • 57
  • 106

6 Answers6

12

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.

riccardo.cardin
  • 7,971
  • 5
  • 57
  • 106
  • I upvoted you for trying, but what you’re doing looks like a hack. It’d be clearer to let go of the `Cacheable` annotation and use the cache manager directly. Or register a callback that does. – Abhijit Sarkar Nov 09 '17 at 07:00
  • The mechanisms under the hood of `@Cacheable` annotation are very useful. I am using Spring, so I am trying to do things following its philosophy. – riccardo.cardin Nov 09 '17 at 07:02
4

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

riccardo.cardin
  • 7,971
  • 5
  • 57
  • 106
sol4me
  • 15,233
  • 5
  • 34
  • 34
  • Thanks for the response. In the Jira that you linked was asked a different question. As far as I understand, in the Jira the user is asking for caching the internal value of the `ListenableFuture`. I think that as long as `CompletableFuture` honors the contract of `Object`, it can be successfully cached, can't it? – riccardo.cardin Nov 07 '17 at 15:25
2

Add @Async annotation on methods in one class and @Cacheable annotation at method level in a different class.

Then invoke @Async method from a service or any different layer.

It worked for me, both Redis cache and Async, which improved the performance drastically.

Didier L
  • 18,905
  • 10
  • 61
  • 103
  • 1
    Hi @Rohit, welcome to Stack Overflow. From what I understand, this is similar to the solution we have also implemented on our project. However I think it would be nice to add a bit of code to illustrate your solution, and be more specific in the descriptions. – Didier L Sep 05 '18 at 13:47
0

In theory, it would work as long as

  • the implementation of CacheManager behind the @Cacheable is not serializing the cached objects (like a cache backed by Hazelcast)

  • Since the CompletableFuture holds a state, which can be modified by calling e.g. the cancel() method, it's important that all the users of the API won't mess around with the cached object. Otherwise, there might be the risk that the cached object inside the Future could not be retrieved anymore, and a cache eviction would be necessary

  • It's worth to verify in which order the proxies behind the annotations are called. i.e. is the @Cacheable proxy called always before the @Async one? Or the other way around? Or it depends? For example, if the @Async is called before, it will fire a Callable inside a ForkJoinPool, just to then retrieve the other object from the cache.

riccardo.cardin
  • 7,971
  • 5
  • 57
  • 106
0

I tried the below approach and it seems to work.

  • create a method with @Cachable which does the actual business logic
  • create a method with @Async which calls the above @Cachable method and returns a CompletableFuture
  • call the method with @Async in your main execution flow

Example:

public class Main {
    public void cachedAsyncData() {
        try {
            asyncFetcher.getData().get();
        } catch(Exception e){}
    }
}

public class AsyncFetcher {
    @Async
    public CompletableFuture<String> getData() {
        return CompletableFuture.completedFuture(cacheFetcher.getData());
    }
}

public class CacheFetcher {
    @Cacheable
    public String getData() {
        return "DATA";
    }
}
ColdSpike
  • 196
  • 1
  • 17
0

Please add annotation @EnableAsync at @Component or @Serice class levele. Exp:

@Service
@Slf4j
@EnableAsync //Add it to here
public class CachingServiceImpl implements CachingService {

Hope to help you!

phancuongviet
  • 311
  • 2
  • 11