Skip to content

Out of memory in test with an active ticker channel #3166

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
lukasz-kalnik-gcx opened this issue Jan 31, 2022 · 8 comments
Closed

Out of memory in test with an active ticker channel #3166

lukasz-kalnik-gcx opened this issue Jan 31, 2022 · 8 comments

Comments

@lukasz-kalnik-gcx
Copy link

lukasz-kalnik-gcx commented Jan 31, 2022

I have a presenter with injected dispatcher, which performs a task periodically based on a ticker Channel:

import kotlinx.coroutines.channels.ticker

class Presenter(
    // other dependencies...
    coroutineContext: CoroutineContext = Dispatchers.Default
) {

    val scope = CoroutineScope(coroutineContext)
    
    init {
        scope.launch {
            val tickerChannel = ticker(delayMillis = 30_000L, initialDelayMillis = 0L, coroutineContext)
            for (ticker in tickerChannel) { requestState() }
        }
    }
}

In the test I inject an UnconfinedTestDispatcher and use runTest but the test doesn't fail with unfinished coroutines. It shows a java.lang.OutOfMemoryError: Java heap space after some time:

class PresenterTest {

    val testScope = TestScope(UnconfinedTestDispatcher())

    @Test
    fun `test requesting state`() = testScope.runTest {
        val presenter = Presenter(
            // other mocked dependencies...
            testScope.coroutineContext
        )
        
        // verify some behavior of the mocked dependencies 
    }
}

JUnit Jupiter 5.7.2
Coroutines 1.6.0
Kotlin 1.6.10

@qwwdfsad
Copy link
Collaborator

qwwdfsad commented Jan 31, 2022

Could you please post a self-contained Presenter source? Consuming from a channel is prohibited in non-suspending contexts, and Presenter init block is non-suspending context

@qwwdfsad
Copy link
Collaborator

From what I can say now -- tickerChannel never completes, so if your requestState creates memory pressure, then it eventually will lead to OOM

@lukasz-kalnik-gcx
Copy link
Author

Could you please post a self-contained Presenter source? Consuming from a channel is prohibited in non-suspending contexts, and Presenter init block is non-suspending context

Sorry, I forgot to wrap consuming from the ticker channel in CoroutineScope.launch. I fixed it now in my example.

@lukasz-kalnik-gcx
Copy link
Author

From what I can say now -- tickerChannel never completes, so if your requestState creates memory pressure, then it eventually will lead to OOM

Shouldn't runTest detect though that there is a non-canceled coroutine at the end of the test and just fail the test?

@lukasz-kalnik-gcx
Copy link
Author

More background: the example is obviously simplified. In the real presenter we use a custom CoroutineScope attached to presenter lifecycle callbacks. It holds a Job reference and cancels the Job when presenter is destroyed.

What I did wrong in my test is I forgot to call presenter.onDestroy() at the end of the test. With this the ticker coroutine gets canceled.
I was wondering though if runTest shouldn't detect the "hanging" coroutine at the end of the test. Or do I understand it wrong?

@qwwdfsad
Copy link
Collaborator

In general, it should.

But tickers are not integrated into structured concurrency (that's why they are obsolete) and thus runTest cannot detect them -- coroutine backing ticker is a top-level one, not registered anywhere.

You can workaround that by using ticker(context = yourScope.coroutineContext), but ideally it should be automatic for tickers. See #540

@lukasz-kalnik-gcx
Copy link
Author

lukasz-kalnik-gcx commented Jan 31, 2022

Yes, I tried ticker(context = scope.coroutineContext) but it still didn't get detected by runTest that the coroutine was running.
Thank you for your fast and helpful responses, I appreciate them very much!

@lukasz-kalnik-gcx
Copy link
Author

BTW is there an alternative to ticker() if they are obsolete?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants