withTimeout function gives IllegalStateException: There is no event loop. Use runBlocking { ... } to start one. in Kotlin Multiplatform iOS client
Asked Answered
D

3

19

Update: It works if I first execute a coroutine without timeout and then withTimeout. But If I execute a coroutine withTimeout first then it gives me an error. same goes for Async as well.

I am creating a demo kotlin multiplatform application where I am executing an API call with ktor. I want to have a configurable timeout function on ktor request so I am using withTimeout at coroutine level.

Here is my function call with network API.

suspend fun <T> onNetworkWithTimeOut(
    url: String,
    timeoutInMillis: Long,
    block: suspend CoroutineScope.() -> Any): T {
    return withTimeout(timeoutInMillis) {
        withContext(dispatchers.io, block)
    } as T
}

suspend fun <T> onNetworkWithoutTimeOut(url: String, block: suspend CoroutineScope.() -> Any): T {
    return withContext(dispatchers.io, block) as T
}

Here is my AppDispatcher class for the iOSMain module.

@InternalCoroutinesApi
actual class AppDispatchersImpl : AppDispatchers {
@SharedImmutable
override val main: CoroutineDispatcher =
    NsQueueDispatcher(dispatch_get_main_queue())

@SharedImmutable
override val io: CoroutineDispatcher =
    NsQueueDispatcher(dispatch_get_main_queue())

internal class NsQueueDispatcher(
    @SharedImmutable private val dispatchQueue: dispatch_queue_t
) : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        NSRunLoop.mainRunLoop().performBlock {
            block.run()
        }
    }
}

}

so the function with the timeout gives me the following error in iOS client.

kotlin.IllegalStateException: There is no event loop. Use runBlocking { ... } to start one.

I am using 1.3.2-native-mt-1 version of the kotlin-coroutine-native. I have created a sample demo application at the following URL. https://github.com/dudhatparesh/kotlin-multiplat-platform-example

Deuteragonist answered 17/12, 2019 at 9:4 Comment(3)
The error is only coming in iOS client? Android client works properly?Kiarakibble
Yes Android client is working perfectly fineDeuteragonist
Am running in to similar issue when trying to update github.com/joreilly/PeopleInSpace to use native mt version of coroutines....trying 1.3.3-native-mt version mentioned in github.com/Kotlin/kotlinx.coroutines/issues/462. Seems we should be using newSingleThreadContext but that doesn't resolve for some reason.Certification
L
9

So, as mentioned in comment above I had similar issue but turned out that it wasn't picking up the native-mt version due to transitive dependencies in other libraries. Added following and it's resolving now.

        implementation('org.jetbrains.kotlinx:kotlinx-coroutines-core-native') 
        {
            version {
                strictly '1.3.3-native-mt'
            }
        }

Also note guidance in https://github.com/Kotlin/kotlinx.coroutines/blob/native-mt/kotlin-native-sharing.md

Starting to make use of this in https://github.com/joreilly/PeopleInSpace

Leper answered 26/12, 2019 at 17:3 Comment(6)
Just tried that. didn't work getting the same error.Deuteragonist
I have added your fix on the repository at github.com/dudhatparesh/kotlin-multiplat-platform-exampleDeuteragonist
Thanks to John's answer I was able to call the below function successfully from iOS ``` @InternalCoroutinesApi fun backgroundTest() { val job = GlobalScope.launch { kprint("we are on main thread \n") withContext(Dispatchers.Default) { kprint("hello \n") delay(2000) kprint("world \n") } } } ```Planetstruck
Hey John. Thanks for this. Any idea how I can get ktor to build then? Any way I can force it to use 1.3.3-native-mt? I get Could not resolve org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.3. Required by: project :shared > io.ktor:ktor-client-ios:1.3.0 > io.ktor:ktor-client-ios-iosx64:1.3.0Mister
@CarsonHolzheimer I'd recommend looking at dependency setup in github.com/joreilly/PeopleInSpaceCertification
@JohnO'Reilly Thanks again. I resolved it by upgrading my gradle version to 6 like you had in the example.Mister
C
4

If you want to use [withTimeout] functions in coroutines you have to modify your Dispatcher to implement Delay interface. Here is an example of how this can be achieved:

@UseExperimental(InternalCoroutinesApi::class)
class UI : CoroutineDispatcher(), Delay {

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatch_get_main_queue()) {
            try {
                block.run()
            } catch (err: Throwable) {
                throw err
            }
        }
    }

    @InternalCoroutinesApi
    override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeMillis * 1_000_000), dispatch_get_main_queue()) {
            try {
                with(continuation) {
                    resumeUndispatched(Unit)
                }
            } catch (err: Throwable) {
                throw err
            }
        }
    }

    @InternalCoroutinesApi
    override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
        val handle = object : DisposableHandle {
             var disposed = false
                private set

            override fun dispose() {
                disposed = true
            }
        }
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeMillis * 1_000_000), dispatch_get_main_queue()) {
            try {
                if (!handle.disposed) {
                    block.run()
                }
            } catch (err: Throwable) {
                throw err
            }
        }

        return handle
    }

}

This solution can be easily modified for your needs.

More information can be found in this thread.

UPDATE

At the moment there is a version 1.3.9-native-mt of kotlinx:kotlinx-coroutines-core artifact which gives the ability to use Dispatchers.Main on ios platform (it supports delay out of the box). It even supports Dispatchers.Default which is used for background work. You can read docs in native-mt branch. Worth noting that the version for ios should be set strictly:

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9-native-mt") {
            version {
                strictly('1.3.9-native-mt')
            }
        }
Champion answered 28/12, 2019 at 9:53 Comment(4)
I tried that solution as well. still, it's giving the same error. however, if I execute any coroutine which does not have timeout before executing the coroutine with timeout it works just fine.Deuteragonist
@PareshDudhat The behaviour you've mentioned is rather strange. There is Dispatchers.Unconfined dispatcher, which have the mechanism rather similar to what you are describing. Are you fully sure about the way you launch your coroutine?Champion
I am launching it with launch(dispatchers.main), I have also tried launching it with dispatcher.main + job but no help. I pushed the latest commit on the GitHub repoDeuteragonist
Beautiful, all i wanted was to call delay in a suspend function and this solution worked like a charm! Thanks @ChampionDystrophy
C
0

Sometimes ios app has a different async requirement with an android app. Use this code for temporary dispatch problem

object MainLoopDispatcher: CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        NSRunLoop.mainRunLoop().performBlock {
            block.run()
        }
    }
}

Please see the forum for this issue: https://github.com/Kotlin/kotlinx.coroutines/issues/470

Cecil answered 26/12, 2019 at 20:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.