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

Paresh Dudhat
  • 1,166
  • 1
  • 14
  • 28
  • The error is only coming in iOS client? Android client works properly? – Kushal Dec 20 '19 at 14:44
  • Yes Android client is working perfectly fine – Paresh Dudhat Dec 21 '19 at 04:14
  • Am running in to similar issue when trying to update https://github.com/joreilly/PeopleInSpace to use native mt version of coroutines....trying `1.3.3-native-mt` version mentioned in https://github.com/Kotlin/kotlinx.coroutines/issues/462. Seems we should be using `newSingleThreadContext` but that doesn't resolve for some reason. – John O'Reilly Dec 26 '19 at 14:42

3 Answers3

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

John O'Reilly
  • 10,000
  • 4
  • 41
  • 63
  • Just tried that. didn't work getting the same error. – Paresh Dudhat Dec 28 '19 at 02:44
  • 1
    I have added your fix on the repository at https://github.com/dudhatparesh/kotlin-multiplat-platform-example – Paresh Dudhat Dec 28 '19 at 02:47
  • 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") } } } ``` – Brendan Weinstein Dec 30 '19 at 20:33
  • 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.0` – Carson Holzheimer Jan 24 '20 at 01:24
  • @CarsonHolzheimer I'd recommend looking at dependency setup in https://github.com/joreilly/PeopleInSpace – John O'Reilly Jan 24 '20 at 09:11
  • 1
    @JohnO'Reilly Thanks again. I resolved it by upgrading my gradle version to 6 like you had in the example. – Carson Holzheimer Jan 25 '20 at 12:19
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')
            }
        }
art
  • 1,222
  • 9
  • 19
  • 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. – Paresh Dudhat Dec 30 '19 at 14:11
  • @PareshDudhat The behaviour you've mentioned is rather strange. There is [`Dispatchers.Unconfined`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html) dispatcher, which have the [mechanism](https://kotlinlang.org/docs/reference/coroutines/coroutine-context-and-dispatchers.html#unconfined-vs-confined-dispatcher) rather similar to what you are describing. Are you fully sure about the way you launch your coroutine? – art Dec 30 '19 at 21:46
  • 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 repo – Paresh Dudhat Dec 31 '19 at 03:20
  • Beautiful, all i wanted was to call delay in a suspend function and this solution worked like a charm! Thanks @art – TheYogi Sep 16 '20 at 19:49
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

antonio yaphiar
  • 450
  • 3
  • 9