具有请求队列的Kotlin服务

时间:2019-04-28 20:14:53

标签: kotlin architecture future deferred kotlin-coroutines

我想使用以下API设计服务:

suspend fun getUsers(request: Request): List<User>

在后台,我会将请求发送到服务器(没关系,但是可以说它是响应式WebClient),但这是一个窍门:我只能每500毫秒发送一次请求,否则会出现错误。

有人可以建议我如何实现该方法,以便当我从协程调用getUsers时,它挂起时,会将工作单元添加到具有此方法的服务的某些队列中,然后在某个时间点并返回结果?

我假设我可以使用一些ReceiveChannel作为队列,并在其中使用for对其元素进行delay循环,但是我有点迷失了放置此逻辑的位置。这是否应该像将永远运行并被getUsers调用的后台方法?可能永远不会调用close方法,因此也可以暂停该方法,但是如何将值从该无限运行方法传递回需要结果的getUsers呢?

编辑

此刻,我正在考虑这样的解决方案:

private const val REQUEST_INTERVAL = 500

@Service
class DelayedRequestSenderImpl<T> : DelayedRequestSender<T> {
    private var lastRequestTime: LocalDateTime = LocalDateTime.now()
    private val requestChannel: Channel<Deferred<T>> = Channel()

    override suspend fun requestAsync(block: () -> T): Deferred<T> {
        val deferred = GlobalScope.async(start = CoroutineStart.LAZY) { block() }
        requestChannel.send(deferred)
        return deferred
    }

    @PostConstruct
    private fun startRequestProcessing() = GlobalScope.launch {
        for (request in requestChannel) {
            val now = LocalDateTime.now()
            val diff = ChronoUnit.MILLIS.between(lastRequestTime, now)
            if (diff < REQUEST_INTERVAL) {
                delay(REQUEST_INTERVAL - diff)
                lastRequestTime = now
            }
            request.start()
        }
    }
}

我在这里看到的问题是,我必须泛化类以使requestChannel通用,因为请求的结果可能是任何东西。但这意味着DelayedRequestSender的每个实例都将绑定到一个特定的类型。关于如何避免这种情况的任何建议?

编辑2

这是精制版本。我目前看到的唯一可能的流程是,如果需要或使用反射,我们必须公开@PostConstruct方法以便编写任何测试。

该想法是不使用GlobalScope,并且也使用单独的Job作为处理方法。这是个好方法吗?

interface DelayingSupplier {
    suspend fun <T> supply(block: () -> T): T
}

@Service
class DelayingSupplierImpl(@Value("\${vk.request.interval}") private val interval: Int) : DelayingSupplier {
    private var lastRequestTime: LocalDateTime = LocalDateTime.now()
    private val requestChannel: Channel<Deferred<*>> = Channel()
    private val coroutineScope = CoroutineScope(EmptyCoroutineContext)

    override suspend fun <T> supply(block: () -> T): T {
        val deferred = coroutineScope.async(start = CoroutineStart.LAZY) { block() }
        requestChannel.send(deferred)
        return deferred.await()
    }

    @PostConstruct
    fun startProcessing() = coroutineScope.launch(context = Job(coroutineScope.coroutineContext[Job])) {
        for (request in requestChannel) {
            val now = LocalDateTime.now()
            val diff = ChronoUnit.MILLIS.between(lastRequestTime, now)
            if (diff < interval) {
                delay(interval - diff)
            }
            lastRequestTime = LocalDateTime.now()
            request.start()
        }
    }
}

2 个答案:

答案 0 :(得分:1)

我建议:

  • 将泛型推到功能级别
  • 使用actor而不是协程实现(但您可能更喜欢这样做)。

无论哪种方式,此解决方案都应允许您使用队列的单个实例来处理所有请求的延迟,而与返回类型无关。 (抱歉,我重命名了一些东西来帮助自己进行概念化,希望这仍然有意义):

private const val REQUEST_INTERVAL = 500

interface DelayedRequestHandler {

    suspend fun <T> handleWithDelay(block: () -> T): T

}

class DelayedRequestHandlerImpl(requestInterval: Int = REQUEST_INTERVAL) : DelayedRequestHandler, CoroutineScope {
    private val job = Job()
    override val coroutineContext = Dispatchers.Unconfined + job
    private val delayedHandlerActor = delayedRequestHandlerActor(requestInterval)

    override suspend fun <T> handleWithDelay(block: () -> T): T {
        val result = CompletableDeferred<T>()
        delayedHandlerActor.send(DelayedHandlerMsg(result, block))
        return result.await()
    }
}

private data class DelayedHandlerMsg<RESULT>(val result: CompletableDeferred<RESULT>, val block: () -> RESULT)

private fun CoroutineScope.delayedRequestHandlerActor(requestInterval: Int) = actor<DelayedHandlerMsg<*>>() {
    var lastRequestTime: LocalDateTime = LocalDateTime.now()
    for (message in channel) {
        try {
            println("got a message processing")
            val now = LocalDateTime.now()
            val diff = ChronoUnit.MILLIS.between(lastRequestTime, now)
            if (diff < requestInterval) {
                delay(requestInterval - diff)
            }
            lastRequestTime = LocalDateTime.now()
            @Suppress("UNCHECKED_CAST")
            val msgCast = message as DelayedHandlerMsg<Any?>
            val result = msgCast.block()
            println(result)
            msgCast.result.complete(result)
        } catch (e: Exception) {
            message.result.completeExceptionally(e)
        }
    }
}


fun main() = runBlocking {
    val mydelayHandler = DelayedRequestHandlerImpl(2000)
    val jobs = List(10) {
        launch {
            mydelayHandler.handleWithDelay {
                "Result $it"
            }
        }
    }
    jobs.forEach { it.join() }
}

答案 1 :(得分:0)

这是我想出的最后一个实现。请注意<div id="app"> <v-menu offset-y> <template v-slot:activator="{ on }"> <v-hover> <v-chip slot-scope="{ hover }" :class="`elevation-${hover ? 5 : 2}`" v-ripple color="indigo" text-color="white" v-on="on"> <v-avatar> <v-icon>account_circle</v-icon> </v-avatar> Sagar </v-chip> </v-hover> </template> <v-list> <v-list-tile v-for="(item, i) in items" :key="i" @click="" > <v-list-tile-title>{{ item.title }}</v-list-tile-title> </v-list-tile> </v-list> </v-menu> </div> <script> new Vue({ el: '#app', data() { return { items: [ { title: 'Click Me' }, { title: 'Click Me' }, { title: 'Click Me' }, { title: 'Click Me 2' } ], } } }) </script> <style> .v-chip .v-chip__content{ cursor: pointer; } </style> ,因为我们不希望在其中一个请求失败的情况下停止处理,这完全有可能(至少在我看来)。

此外,@ Laurence建议的选项可能会更好,但是由于API被标记为过时,我决定暂时不使用actor。

SupevisorJob