🐧

CaffeineをKotlin Coroutinesでも使えるようにする方法 - caffeine-coroutinesの紹介

2025/02/02に公開

Motivation

JVMには、キャッシュを実装するときに広く使われているCaffeineというライブラリがあります。

https://github.com/ben-manes/caffeine

このライブラリはJavaで書かれているため、Coroutines上ではそのままだと使用することができません。

CaffeineにはAsyncサポート(ComletableFutureに対応したインタフェース)があるため、これを駆使することで一応Coroutines上でも動作させることが可能になります。ただし、CaffeineとCoroutinesへの理解が深くないとなかなか敷居が高いのかなと思います。

実際に、Stack OverflowでもCoroutines上での使い方についての質問もあり、苦労している人も一定数いるようです。
https://stackoverflow.com/questions/71666175/what-is-the-preferred-way-how-to-add-caffeine-cache-to-kotlin-with-coroutines

そこで、誰もが簡単に使えるようにcaffeine-coroutinesというライブラリを作っています。

https://github.com/be-hase/caffeine-coroutines

※ CaffeineのREADMEでも紹介してもらっています。

基本的な使い方

なにはともあれ、まずは依存にいれましょう。

implementation("dev.hsbrysk:caffeine-coroutines:{{version}}")

使い方ですが、基本的にオリジナルのCaffeineとほとんど差分はありません。新しく覚えることは、buildCoroutineを呼び出すことでCoroutines上で使えるCoroutineCacheインスタンスを得ることができるということだけです。

suspend fun main() {
    val cache: CoroutineCache<String, String> = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(Duration.ofMinutes(5))
        .buildCoroutine() // buildCoroutineを使う

    val value = cache.get("key") {
        delay(1000) // suspend functionが使えるようになる
        "value"
    }
    println(value)
}

もちろん、Loading Cache スタイルもサポートしています。buildCoroutineにloaderを渡すと、CoroutineLoadingCacheインスタンスを得ることができます。

suspend fun main() {
    val cache: CoroutineLoadingCache<String, String> = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(Duration.ofMinutes(5))
        .buildCoroutine { // buildCoroutineを使う
            delay(1000) // suspend functionが使えるようになる
            "value"
        }

    val value = cache.get("key")
    println(value)
}

Philosophy

Caffeineではblocking用途のための実装としてCacheLoadingCacheがあり、async(CompletableFuture)用途のための実装としてAsyncCacheAsyncLoadingCacheがあります。

このライブラリはその名の通り、Coroutines用途の実装としてCoroutineCacheCoroutineLoadingCacheを提供することだけに焦点を当てています。

独自のAPI/インタフェースを導入することは必要最低限にしています。
独自のAPI/インタフェースを導入すると、利用者を混乱させてしまいます。導入も難しくなりますし、利用を廃止するときにも面倒なことになるでしょう。

Aedileとの比較 (類似実装との比較)

実はAedileという、CaffeineのKotlin Wrapperもあります。

が、自分がcaffeine-coroutinesを実装した時点だと、以下の問題を感じていました。
(もっともこの問題の多くは最近リリースされたversion 2から解決されているので、Aedileを使っても良いでしょう)

  • Coroutine Scopeの取り扱いに関する問題
    • 本来、呼び出し元のScopeから派生するScopeで実行すべきなのですが(じゃないと例えばCoroutine Contextなどが引き継がれない)、なぜか別途のScopeを利用する形式となっていました。
    • 結果として、MDCなどの引き継ぎに問題がありました。
  • Coroutineのキャンセルに関する問題
    • 前述したCoroutine Scopeの取り扱いに関する問題は後続のバージョンで対応されたのですが、loader内で例外が発生すると、呼び出し元のCoroutineもキャンセルされてしまうという問題がありました。
    • これはversion 2で修正されていますが、修正方法が少し変ですね...。(coroutineScope funを使って子scopeを作るだけでいいはずです)
  • 独自のAPIが多かった
    • Cacheを作るために、caffeineBuilderという独自のビルダーを使う必要がありました。
      • 普段使い慣れている公式のビルダーをそのまま使ったほうがわかりやすいですし、結果として本家の実装にある機能を全て使うことができませんでした。
      • この部分については、version 2からcaffeine-coroutinesと同じようなスタイルを採用したことで解決はされているようです。
    • また、metricsに関してもCacheMetricsという独自のAPIを使う必要があり、そのあたりも良くないと感じていました。これについてもversion 2からはDeprecatedされているようです。

まとめ

caffeine-coroutinesは、CaffeineをCoroutines上で簡単に使えるようにするライブラリです。buildCoroutine拡張関数を使うことで、Caffeineの機能をそのままCoroutines上で扱えます。

類似実装のAedileも改善されていますが、過去にはCoroutine Scopeの扱いやキャンセル処理、独自APIの多さといった課題がありました。caffeine-coroutinesは公式APIを活かしたシンプルな設計が特徴です。

Coroutines環境でCaffeineを使いたい方は、ぜひ試してみてください。

Discussion