0

I am using caffeine cache to store an authorisation token that has been obtained using webClient WebFlux. I have set the expireAfterWrite to a hardcoded value in the application.yml file as follows:

spring:
  cache:
    cache-names: accessTokens
    caffeine:
      spec: expireAfterWrite=100m

The token is obtained using a WebClient with Spring WebFlux as below code depicts:

 @Autowired
 var cacheManager: CacheManager? = null

 override fun getAuthToken(): Mono<AccessToken> {

    val map = LinkedMultiValueMap<String, String>()
    map.add("client_id", clientId)
    map.add("client_secret", clientSecret)
    map.add("grant_type", "client_credentials")

    var cachedVersion = this.cacheManager?.getCache("accessTokens");
    if (cachedVersion?.get("tokens") != null) {
        val token = cachedVersion.get("tokens")
        return Mono.just(token?.get() as AccessToken)
    } else {

        return webClient.post()
                .uri("/client-credentials/token")
                .body(BodyInserters.fromFormData(map))
                .retrieve()
                .onStatus(HttpStatus::is5xxServerError) {
                    ClientLogger.logClientErrorResponse(it, errorResponse)
                }
                .onStatus(HttpStatus::is4xxClientError) {
                    ClientLogger.logClientErrorResponse(it, errorResponse)
                }
                .bodyToMono(AccessToken::class.java)
                .doOnNext { response ->       
                      // Set here the expiration time of the cache based on  
                      // response.expiresIn              
                      this.cacheManager?.getCache("accessTokens")?.put("tokens", response) }
                .log()
    }

}

I am storing the token after the data is emitted/returned successfully within the .doOnNext() method but i need to be able to set the expiration time or refresh the hardcoded expiration time of the cache based on the expiresIn property that is part of the response object,

      .doOnNext { response ->    
                      // Set here the expiration time of the cache based on  
                      // response.expiresIn           
                      this.cacheManager?.getCache("accessTokens")?.put("tokens", response) 
                 }

Any ideas would be much appreciated.

D.B
  • 4,009
  • 14
  • 46
  • 83
  • Using the native APIs, this can be done by converting between `Mono` and `CompletableFuture`, using `AsyncCache`, and evaluating the entry via `expireAfter(Expiry)`. When the future materializes then it will calculate the value's expiration time. Spring Cache may be too limiting as it is not intended for anything beyond the simplest use cases. – Ben Manes Jan 21 '21 at 08:57
  • Thanks @BenManes. Could u please implement an example of that approach or maybe do u know an article i can have a look to find a similar solution to what you propose. Thanks. – D.B Jan 21 '21 at 12:35

1 Answers1

2
// Policy to set the lifetime based on when the entry was created
var expiresAfterCreate = new Expiry<String, AccessToken>() {
  public long expireAfterCreate(String credentials, AccessToken token, long currentTime) {
    Duration duration = token.expiresIn();
    return token.toNanos();
  }
  public long expireAfterUpdate(String credentials, AccessToken token, 
      long currentTime, long currentDuration) {
    return currentDuration;
  }
  public long expireAfterRead(String credentials, AccessToken token,
      long currentTime, long currentDuration) {
    return currentDuration;
  }
});

// CompletableFuture-based cache
AsyncLoadingCache<String, AccessToken> cache = Caffeine.newBuilder()
    .expireAfter(expiresAfterCreate)
    .buildAsync((credentials, executor) -> {
      Mono<AccessToken> token = retrieve(credentials);
      return token.toFuture();
    });

// Get from cache, loading if absent, and converts to Mono
Mono<AccessToken> getAuthToken() {
  var token = cache.get(credentials);
  return Mono.fromFuture(token);
}
Ben Manes
  • 9,178
  • 3
  • 35
  • 39
  • thanks Ben, within the method getAuthToken() if token is absent (cache is empty) i should be able to call webClient to retrieve the token right, but how can i put it in the cache when it has been retrieved. I am doing something like that but never gets populated: .doOnNext { response -> cache?.put("accessTokens",Mono.just(response).toFuture()) } – D.B Jan 22 '21 at 03:01
  • @D.B In this example, the `buildAsync` method accepts a loading function that is called on a miss. This mirrors `Map.computeIfAbsent`, which is available if you prefer it at the call-site. Here the cache is populated with the future on the miss and returns it until the entry expires, where the next call will miss and it repeats. – Ben Manes Jan 22 '21 at 04:53
  • I don't know Reactor or Spring well, but your sample populates the cache when the response materializes. In mine the in-flight future is cached, which avoids "cache stampedes" when multiple threads perform concurrent calls, miss, and load. If the future fails or resolves to `null` then a callback removes it from the cache. If successful, then the callback will call `Expiry` to set the entry's duration. – Ben Manes Jan 22 '21 at 04:57
  • Thanks @ben. the issue of populating the cache when building it is that the object cache is the full webClient call and not the response obtained by that webclient after it has been materialised, therefore a new token is obtained from the cache everytime. Is there not a method to set up the expireAfter property from caffeine using the CacheManager service from springboot? – D.B Jan 28 '21 at 15:21
  • @D.B In SpringBoot you can set the cache builder in code ([tutorial](https://www.baeldung.com/spring-boot-caffeine-cache)), which should allow you to set `expireAfter`. Since the cache needs to inspect something for the expiration time, either you would need to include it on the value (e.g. a wrapper) or look it up elsewhere. Otherwise there is `put(k, v, duration)` and `putIfAbsent(k, v, duration)` under `cache.policy().expireVariably()` for more manual control. – Ben Manes Jan 28 '21 at 20:09
  • when injecting the CacheManager service, the methods put and putIfAbsent do not have duration as part of the signature. I have created CaffeineCache bean that is defining the main configuration for my cache but I do not know how Can I make that cache.policy().expireVariably() part of my code. Sorry and thanks again for your support. – D.B Jan 28 '21 at 23:29
  • @D.B You would have to use the native apis for that. The Spring annotations use a custom key. It really isn't meant for advanced cases, like async. I don't know if it makes sense to use spring cache in your scenario. – Ben Manes Jan 28 '21 at 23:31