协程的长寿服务

时间:2018-12-13 15:24:28

标签: kotlinx.coroutines

我想创建一个可以处理事件的长期服务。 它通过postEvent接收事件,将其存储在存储库中(带有底层数据库),并在事件足够多时以api的形式发送它们。

我也想按需关闭它。 此外,我想测试这项服务。

这是我到目前为止提出的。目前,我正在努力对其进行单元测试。 通过fixture.postEvent()将事件发送到服务后,数据库可能会过早关闭,或者测试本身陷入某种死锁状态(正在尝试各种上下文+作业配置)。

我在这里做什么错了?

class EventSenderService(
        private val repository: EventRepository,
        private val api: Api,
        private val serializer: GsonSerializer,
        private val requestBodyBuilder: EventRequestBodyBuilder,
) : EventSender, CoroutineScope {

    private val eventBatchSize = 25

    val job = Job()
    private val channel = Channel<Unit>()

    init {
        job.start()

        launch {
            for (event in channel) {
                val trackingEventCount = repository.getTrackingEventCount()

                if (trackingEventCount < eventBatchSize) continue

                readSendDelete()
            }
        }
    }

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Default + job

    override fun postEvent(event: Event) {
        launch(Dispatchers.IO) {
            writeEventToDatabase(event)
        }
    }

    override fun close() {
        channel.close()
        job.cancel()
    }

    private fun readSendDelete() {
        try {
            val events = repository.getTrackingEvents(eventBatchSize)

            val request = requestBodyBuilder.buildFor(events).blockingGet()

            api.postEvents(request).blockingGet()

            repository.deleteTrackingEvents(events)
        } catch (throwable: Throwable) {
            Log.e(throwable)
        }
    }

    private suspend fun writeEventToDatabase(event: Event) {
        try {
            val trackingEvent = TrackingEvent(eventData = serializer.toJson(event))
            repository.insert(trackingEvent)
            channel.send(Unit)
        } catch (throwable: Throwable) {
            throwable.printStackTrace()
            Log.e(throwable)
        }
    }
}

测试

@RunWith(RobolectricTestRunner::class)
class EventSenderServiceTest : CoroutineScope {

    @Rule
    @JvmField
    val instantExecutorRule = InstantTaskExecutorRule()

    private val api: Api = mock {
        on { postEvents(any()) } doReturn Single.just(BaseResponse())
    }
    private val serializer: GsonSerializer = mock {
        on { toJson<Any>(any()) } doReturn "event_data"
    }
    private val bodyBuilder: EventRequestBodyBuilder = mock {
        on { buildFor(any()) } doReturn Single.just(TypedJsonString.buildRequestBody("[ { event } ]"))
    }
    val event = Event(EventName.OPEN_APP)

    private val database by lazy {
        Room.inMemoryDatabaseBuilder(
                RuntimeEnvironment.systemContext,
                Database::class.java
        ).allowMainThreadQueries().build()
    }

    private val repository by lazy { database.getRepo() }

    val fixture by lazy {
        EventSenderService(
                repository = repository,
                api = api,
                serializer = serializer,
                requestBodyBuilder = bodyBuilder,
        )
    }

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Default + fixture.job

    @Test
    fun eventBundling_success() = runBlocking {

        (1..40).map { Event(EventName.OPEN_APP) }.forEach { fixture.postEvent(it) }

        fixture.job.children.forEach { it.join() }

        verify(api).postEvents(any())
        assertEquals(15, eventDao.getTrackingEventCount())
    }
}

按照@Marko Topolnik的建议更新代码后-添加fixture.job.children.forEach { it.join() }测试永远不会完成。

1 个答案:

答案 0 :(得分:1)

您做错的一件事与此有关:

override fun postEvent(event: Event) {
    launch(Dispatchers.IO) {
        writeEventToDatabase(event)
    }
}

postEvent启动即发即弃的异步作业,该作业最终会将事件写入数据库。您的测试会快速连续创建40个此类作业,并且在排队时会声明预期状态。不过,我不知道为什么您要在发布40个事件后断言15个事件。

要解决此问题,您应该使用已有的行:

fixture.job.join()

但将其更改为

fixture.job.children.forEach { it.join() }

将其放低,在创建事件的循环之后。


我没有考虑到您在init块中启动的长期消费者工作。这使我上面提出的加入主工作的所有孩子的建议无效。

相反,您将不得不进行更多更改。使postEvent返回它启动的作业,并在测试中收集所有这些作业并加入。这样更具选择性,并且避免参加长期工作。


作为一个单独的问题,您的批处理方法并不理想,因为在执行任何操作之前,它将始终等待完整的批处理。每当出现无事件的休整期时,事件将无限期地处于不完整的批次中。

最好的方法是自然批处理,在这种情况下,您将不断消耗输入队列。当有大量传入事件发生时,批次自然会增长,当它们滴入时,仍将立即提供。您可以看到基本概念here