如何正确启动Kotlin协程以实现Caffeine AsyncLoadingCache?

时间:2019-03-20 21:52:22

标签: kotlin kotlin-coroutines caffeine

我有一个使用协程的Kotlin JVM服务器应用程序,我需要将缓存放在非阻塞网络调用的前面。我认为我可以使用咖啡因AsyncLoadingCache来获得所需的非阻塞缓存行为。我需要实现的AsyncCacheLoader接口使用CompletableFuture。同时,我要调用的用于加载缓存项的方法是一个suspend函数。

我可以这样缩小差距:

abstract class SuspendingCacheLoader<K, V>: AsyncCacheLoader<K, V> {
    abstract suspend fun load(key: K): V

    final override fun asyncLoad(key: K, executor: Executor): CompletableFuture<V> {
        return GlobalScope.async(executor.asCoroutineDispatcher()) {
            load(key)
        }.asCompletableFuture()
    }
}

这将在提供的load(默认为Executor)上运行ForkJoinPool函数,从咖啡因的角度来看,这是正确的行为。

但是,我知道我应该尝试avoid using GlobalScope to launch coroutines

我考虑让自己的SuspendingCacheLoader实现CoroutineScope并管理自己的协程上下文。但是CoroutineScope旨在由具有托管生命周期的对象实现。缓存和AsyncCacheLoader都没有任何生命周期挂钩。缓存拥有ExecutorCompletableFuture实例,因此它已经以这种方式控制了加载任务的生命周期。我看不到将任务归于协程上下文会添加任何内容,而且我担心在停止使用缓存后无法正确关闭协程上下文。

编写自己的异步缓存机制非常困难,因此,如果可以的话,我想与Caffeine实现集成。

是使用GlobalScope来实现AsyncCacheLoader的正确方法,还是有更好的解决方案?

5 个答案:

答案 0 :(得分:3)

  

缓存拥有Executor和CompletableFuture实例,因此它已经以这种方式控制了加载任务的生命周期。

这是不正确的,Caffeine上的文档指定它使用了用户提供的ExecutorForkJoinPool.commonPool()(如果未提供)。这意味着没有默认生命周期。

无论直接调用GlobalScope都是错误的解决方案,因为没有理由对选择进行硬编码。只需通过构造函数提供CoroutineScope并使用GlobalScope作为参数,而您没有明确的生命周期可以绑定到缓存。

答案 1 :(得分:0)

经过深思熟虑,我想出了一个不同的解决方案,该解决方案将继承用于调用get方法的任何上下文,而不是依赖从头创建或传递给缓存加载器的固定上下文。

该方法通过使用AsyncCache.get而不是AsyncCacheLoader来工作。要获取当前上下文,我使用全局coroutineContext属性。

suspend fun <K: Any, V> AsyncCache.getSuspending(key: K): V {
    val outerContext = coroutineContext
    return get(key) { k, executor ->
        val innerContext = outerContext + Job() + executor.asCoroutineDispatcher()
        CoroutineScope(innerContext).async {
            loadValue(k) // loadValue is a suspend function defined elsewhere
        }.asCompletableFuture()
    }.await()
}

用于调用loadValue的上下文由三件事组成:

  • 用于调用getSuspending
  • 的原始上下文
  • 新的Job(),它将在外部上下文中覆盖作业。必须确保在loadValue中引发的错误不会立即传播,而是会在CompletableFuture中捕获,以便缓存可以适当地响应它们。
  • executor,因此仍然尊重缓存关于线程的配置。

这确实意味着上下文对从何处调用get很敏感,如果缓存由应用程序的不同部分共享,则可能不是正确的行为。但是,我认为这种行为比我原来的方法要好。

对此表示赞赏和评论!

答案 2 :(得分:0)

这是我的解决方法:

定义CoroutineVerticle的扩展功能

fun <K, V> CoroutineVerticle.buildCache(configurator: Caffeine<Any, Any>.() -> Unit = {}, loader: suspend CoroutineScope.(K) -> V) = Caffeine.newBuilder().apply(configurator).buildAsync { key: K, _ ->
    // do not use cache's executor
    future {
        loader(key)
    }
}

CoroutineVerticle内创建我们的缓存

val cache : AsyncLoadingCache<String, String> = buildCache({
  maximumSize(10_000)
  expireAfterWrite(10, TimeUnit.MINUTES)
}) { key ->
    // load data and return it
    delay(1000)
    "data for key: $key"
}

使用缓存

suspend fun doSomething() {
    val data = cache.get('key').await()

    val future = cache.get('key2')
    val data2 = future.await()
}

答案 3 :(得分:0)

这是一个简单的解决方案。用您的类型替换K,V表示法。

    val cache = Caffeine.newBuilder().buildAsync<K, V> { key: K, _ ->
      val future = CompletableFuture<V>()

      launch {
        val result = someAwaitOperation(key)
        future.complete(result)
      }

      future
    }

答案 4 :(得分:0)

建议这样的扩展方法

suspend inline fun <K: Any, V: Any> Caffeine<Any, Any>.suspendingLoadingCache(
    crossinline suspendedLoader: suspend (key: K) -> V
): AsyncLoadingCache<K, V> =
    buildAsync { key, executor: Executor ->
        CoroutineScope(executor.asCoroutineDispatcher()).future {
            suspendedLoader(key)
        }
    }

不建议GlobalScope,请使用CoroutineScope(executor.asCoroutineDispatcher())

future方法在kotlinx-coroutines-jdk8模块中定义