ktor client iOS hangs forever in runBlocking

Created on 26 Oct 2018  路  20Comments  路  Source: ktorio/ktor

commonMain

expect fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T

fun getGitHub(): HttpClientCall = runBlocking {
    HttpClient().call("https://www.github.com")
}

iosMain

actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
    return kotlinx.coroutines.runBlocking(context, block)
}

image

Xcode hangs forever when invoking the method:

func testRunBlocking() {
  GitHubKt.getGitHub()
}

If I delete HttpClient in runBlocking then the method successfully returns a value on iOS.

Versions:

kotlin_version=1.3.0-rc-190
coroutines_version=1.0.0-RC1
ktor_version=1.0.0-beta-2
serialization_version=0.8.2-rc13

Is it a known issue that ktor iOS doesn't work in runBlocking? Is this even a ktor issue or a coroutines issue? https://github.com/Kotlin/kotlinx.coroutines/issues/462

question

Most helpful comment

I defined a custom runBlocking method using NSRunLoop and that works.

https://github.com/bootstraponline/run_blocking/commit/a7953192c7078ab41dbf382f56bdc4432a46c84b#diff-c1a933ca71f0c706f65401b240f8c806

// Expectation.kt
package example

import platform.Foundation.NSDate
import platform.Foundation.NSRunLoop
import platform.Foundation.addTimeInterval
import platform.Foundation.runUntilDate

class Expectation<T> {
    private var waiting = true
    private var result: T? = null

    fun fulfill(result: T?) {
        waiting = false
        this.result = result
    }

    fun wait(): T? {
        while (waiting) {
            advanceRunLoop()
        }

        return result
    }
}

private fun advanceRunLoop() {
    val date = NSDate().addTimeInterval(1.0) as NSDate
    NSRunLoop.mainRunLoop.runUntilDate(date)
}
// RunBlocking.kt
package example

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.launch
import platform.Foundation.NSRunLoop
import platform.Foundation.performBlock
import kotlin.coroutines.CoroutineContext

actual fun <T> runBlocking(block: suspend () -> T): T {
    val expectation = Expectation<T>()

    GlobalScope.launch(MainRunLoopDispatcher) {
        expectation.fulfill(block.invoke())
    }

    return expectation.wait() ?: throw RuntimeException("runBlocking failed")
}

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

All 20 comments

Why do you use runBlocking on ios? You should never block main loop.

Unfortunately Dispatchers.Main doesn't work with ios yet. See https://github.com/Kotlin/kotlinx.coroutines/issues/470

I want to block the main loop. I am using this in the context of a testing library that's run in a separate process from the application. Specifically this is an iOS XCUITest.

My confusion is that the code doesn't work. I am not finding any possible way to fix (even using a background thread).

See Kotlin/kotlinx.coroutines#470

I tried that solution as well, using the custom MainLoopDispatcher and runBlocking never returns when used with ktor.

Well, running coroutines on background threads are not yet supported. Using runBlocking is quite dangerous. And for sure you can't use runBlocking with MainLoopDispatcher and you are already on main loop. Are you sure you actually need blocking?

Are you sure you actually need blocking?

Yeah, I'm replacing an existing blocking networking client (based on gRPC Swift). This is all test code that runs in a different process from the app. It's not related to production at all.

~One possible reason is that the client is trying to resume on main loop while it is blocked~ Also note that a client need to be closed

UPD: looks like ios client is running on unconfined dispatcher

However it makes no difference. ios client is configured to schedule callback to the main loop that is blocked. This is why it doesn't work. So there is no way to use runBlocking with ktor client.

Why can't you simply make getGitHub suspend?

Your fix looks too verbose, you can use use function for client to get it closed

val result = HttpClient().use { it.get<String>("https://.... ") }

there is no way to use runBlocking with ktor client.

Thanks for clarification.

Why can't you simply make getGitHub suspend?

How do I call a Kotlin suspend function in a blocking way from Swift? The beauty of runBlocking is the calls are synchronous from the consumer perspective.

When defining getGitHub as suspend, there's no method generated in run_blocking.h From Swift, the method doesn't exist.

suspend fun getGitHub(): HttpClientCall {
    return HttpClient().use { it.call("https://www.github.com") }
}

I guess iOS is not working yet?

Currently, there's no way to have coroutines in Objective-C or Swift, so exposing suspend functions look tricky.

https://github.com/JetBrains/kotlin-native/issues/1684

Yes, for now there is no way to call suspend functions from swift. However you can invoke swift functions from kotlin coroutines.

ktor client works on ios but you can't mix it with runBlocking and all coroutines need to be launched on a customized coroutine dispatcher that dispatch everything on the main loop

The simplified example:

common.kt

interface MyAppView {
    fun onDataLoadComplete(text: String)
}

class MyPresenter(private val view: MyAppView)  {
    fun load() {
        launch {
            val result = client.get<String>("http://....")
            view.onDataLoadComplete(result)
        }
    }
}

MyAppViewIos.swift

class MyAppViewIos:  ....  , MyAppView {
    lazy var presenter: MyPresenter = { 
        MyPresenter(view: self)
    }

    func somethingClicked() {
        presenter.load()
    }

    func onDataLoadComplete(text: String) { // invoked from presenter's coroutine
        // show result
    }
}

See complete example here: https://github.com/JetBrains/kotlinconf-app

Relevant files are:

view interface (Kotlin): https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/commonMain/kotlin/org/jetbrains/kotlinconf/presentation/SessionListView.kt

presenter (Kotlin)
https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/commonMain/kotlin/org/jetbrains/kotlinconf/presentation/SessionListPresenter.kt

view implementation (Swift)
https://github.com/JetBrains/kotlinconf-app/blob/master/konfios/konfswift/ui/SessionsViewController.swift

UI dispatcher implementation
https://github.com/JetBrains/kotlinconf-app/blob/master/konfios/konfswift/ui/UI.swift

there is no way to call suspend functions from swift. However you can invoke swift functions from kotlin coroutines.

I think a synchronous REST API defined in Kotlin and called by Swift is blocked then. Probably this will be possible once multithreaded support lands.

The callback approach is interesting. I see that as a good fit for writing apps.

Thanks for all the info.

The async callback code works. With the callback API, it doesn't seem possible to wait for operations to finish. I tried using an operation queue and that crashed.

I defined a custom runBlocking method using NSRunLoop and that works.

https://github.com/bootstraponline/run_blocking/commit/a7953192c7078ab41dbf382f56bdc4432a46c84b#diff-c1a933ca71f0c706f65401b240f8c806

// Expectation.kt
package example

import platform.Foundation.NSDate
import platform.Foundation.NSRunLoop
import platform.Foundation.addTimeInterval
import platform.Foundation.runUntilDate

class Expectation<T> {
    private var waiting = true
    private var result: T? = null

    fun fulfill(result: T?) {
        waiting = false
        this.result = result
    }

    fun wait(): T? {
        while (waiting) {
            advanceRunLoop()
        }

        return result
    }
}

private fun advanceRunLoop() {
    val date = NSDate().addTimeInterval(1.0) as NSDate
    NSRunLoop.mainRunLoop.runUntilDate(date)
}
// RunBlocking.kt
package example

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.launch
import platform.Foundation.NSRunLoop
import platform.Foundation.performBlock
import kotlin.coroutines.CoroutineContext

actual fun <T> runBlocking(block: suspend () -> T): T {
    val expectation = Expectation<T>()

    GlobalScope.launch(MainRunLoopDispatcher) {
        expectation.fulfill(block.invoke())
    }

    return expectation.wait() ?: throw RuntimeException("runBlocking failed")
}

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

Thanks for the idea :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

baruchn picture baruchn  路  3Comments

evgfilim1 picture evgfilim1  路  4Comments

gabin8 picture gabin8  路  3Comments

SimonSchubert picture SimonSchubert  路  4Comments

KennethanCeyer picture KennethanCeyer  路  4Comments