0

I am trying to implement a request cache so I can avoid expensive API calls as much as possible.

Currently I have implemented a caching system using Caffeine like so:

@Service
class CacheService {

    val playlistCache: Cache<String, Playlist> = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(30, TimeUnit.SECONDS)
            .build()

    fun queryPlaylistCache(playlistCacheKey: String) =
            Mono.justOrEmpty(playlistCache.getIfPresent(playlistCacheKey)).map<Signal<out Playlist>> { Signal.next(it) }

    val userSavedSongsCache: Cache<String, List<PlaylistOrUserSavedTrack>> = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(30, TimeUnit.SECONDS).build()
    
}

@Service
class SpotifyRequestService(
        val webClients: WebClients,
        val cacheService: CacheService
) {
    fun getAPlaylist(Authorization: String, playlistId: String, fields: String?): Mono<Playlist> {
        return CacheMono.lookup({ key: String -> cacheService.queryPlaylistCache(key) }, "${playlistId}$fields")
                .onCacheMissResume(
                        webClients.spotifyClientServiceClient.get()
                                .uri { uriBuilder: UriBuilder ->
                                    uriBuilder.path("/playlists/{playlist_id}")
                                            .queryParam("fields", fields ?: "")
                                            .build(playlistId)
                                }
                                .header("Authorization", Authorization)
                                .retrieve()
                                .bodyToMono(Playlist::class.java)
                )
                .andWriteWith { key, value ->
                    Mono.fromRunnable { value?.get()?.let { cacheService.playlistCache.put(key, it) } } }
    }
}

However, from what I have read, it seems like implementing caching this way is not optimal because getting/setting the cache is a blocking operation.

However, in this thread: Cache the result of a Mono from a WebClient call in a Spring WebFlux web application the chosen answer mentions reasons why this is an acceptable use case to use a blocking operation.

Can anybody shed some light as to what the correct solution is?

jgv115
  • 499
  • 5
  • 17
  • 2
    CacheMono uses a getIfPresent and put, performing the load outside of the cache so as to be non-blocking. Unfortunately that allows for cache stampedes where multiple callers perform redundant work (get is blocking). AsyncCache blocks only long enough to stores a future, which a Mono can convert to/from. If the future fails it will be automatically removed from the cache. That would seem like the best integration strategy. – Ben Manes Aug 12 '20 at 16:13
  • 1
    as stated in the linked question, as caffeine is an in memory, on heap cache the operations to write/read aren't blocking IO calls. You can install BlockHound (https://github.com/reactor/BlockHound) if you are ever unsure if an operation is blocking or not. – Michael McFadyen Aug 12 '20 at 17:53

0 Answers0