如何将异步缓存与Kotlin协同路由一起使用?

如何将异步缓存与Kotlin协同路由一起使用?,kotlin,caching,kotlin-coroutines,caffeine,Kotlin,Caching,Kotlin Coroutines,Caffeine,我有一个使用协同路由的Kotlin JVM服务器应用程序,我需要在非阻塞网络调用之前放置一个缓存。我想我可以用咖啡因来获得我需要的非阻塞缓存行为。我需要实现的接口使用CompletableFuture。同时,我想调用的加载缓存项的方法是suspend函数 我可以这样弥合差距: abstract class SuspendingCacheLoader<K, V>: AsyncCacheLoader<K, V> { abstract suspend fun load(

我有一个使用协同路由的Kotlin JVM服务器应用程序,我需要在非阻塞网络调用之前放置一个缓存。我想我可以用咖啡因来获得我需要的非阻塞缓存行为。我需要实现的接口使用
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()
    }
}
抽象类SuspendingCacheLoader:AsyncCacheLoader{
摘要暂停娱乐负载(键:K):V
最终重载(键:K,执行者:执行者):CompletableFuture{
返回GlobalScope.async(executor.ascoroutinedpatcher()){
加载(键)
}.asCompletableFuture()
}
}
这将在提供的
执行器上运行
load
功能(默认情况下,
ForkJoinPool
),从咖啡因的角度来看,这是正确的行为

然而,我知道我应该努力

我考虑让我的
SuspendingCacheLoader
实现并管理自己的协同程序上下文。但是
CoroutineScope
旨在由具有托管生命周期的对象实现。缓存和
AsyncCacheLoader
都没有任何生命周期挂钩。缓存拥有
Executor
CompletableFuture
实例,因此它已经通过这种方式控制加载任务的生命周期。我看不出让任务归协同路由上下文所有会增加什么,我担心在缓存停止使用后无法正确关闭协同路由上下文

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

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

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

但事实并非如此,
caffine
上的文档指定它使用用户提供的
Executor
ForkJoinPool.commonPool()
(如果未提供)。这意味着没有默认的生命周期


不管怎样,直接调用
GlobalScope
似乎是错误的解决方案,因为没有理由硬编码选择。只需通过构造函数提供一个
CoroutineScope
,并使用
GlobalScope
作为参数,而缓存没有显式的生命周期可绑定。

经过一番思考,我想出了一个更简单的解决方案,我认为它更习惯于使用coroutines

该方法通过使用而不是实现
AsyncCacheLoader
工作。但是,它会忽略缓存配置为使用的
执行器
,遵循此处其他一些答案的建议

类SuspendingCache(私有val asyncCache:asyncCache){
暂停乐趣获取(键:K):V=主管范围{
getAsync(key).await()
}
private-fun-CoroutineScope.getAsync(key:K)=asyncCache.get(key){K,->
未来{
荷载值(k)
}
}
private-suspend-fun-loadValue(键:K):V=TODO(“加载值”)
}
请注意,这取决于
future
coroutine生成器和
await()
函数的
kotlinx-coroutines-jdk8

我认为忽略执行者可能是正确的选择。正如@Kiskae指出的,缓存在默认情况下将使用
ForkJoinPool
。选择使用它而不是默认的协程调度器可能没有什么用处。但是,如果我们愿意,可以通过更改
getAsync
函数轻松使用它:

private-fun-CoroutineScope.getAsync(key:K)=asyncCache.get(key){K,executor->
future(executor.ascoroutinedpatcher()){
荷载值(k)
}
}
以下是我的解决方案:

定义
coroutinerticle

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)
    }
}
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()
}

这里有一个简单的解决方案。用您的类型替换K,V符号

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

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

      future
    }
val cache=caffee.newBuilder().buildAsync{key:K,->
val future=CompletableFuture()
发射{
val结果=操作(键)
未来。完成(结果)
}
未来
}

建议这样的扩展方法

suspend inline.suspendingLoadingCache(
交叉线悬挂装载机:悬挂(键:K)->V
):AsyncLoadingCache=
buildAsync{key,executor:executor->
CoroutineScope(executor.asCoroutineDispatcher()).future{
吊杆装载机(钥匙)
}
}
不建议使用
GlobalScope
,请使用
CoroutineScope(executor.ascoroutinedipatcher())


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

感谢您的回复和对咖啡因执行者的澄清。我喜欢在构造函数中接受
CoroutineScope
。如果我最终通过了来自更广泛应用程序的
CoroutineScope
,这会给我带来什么好处?我设想在
异步缓存加载器
中,我希望覆盖
调度程序
(使用提供的
执行器
)和
作业
(因为缓存加载失败传播到缓存之外没有意义)。一旦这样做了,似乎就不会有太多的原始上下文了。我不知道传入的协同路由作用域会给您带来什么好处,因为您显然是在协同路由世界之外工作,而您的调用方手头没有它。