Skip to content
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

ktor client iOS hangs forever in runBlocking #678

Closed
bootstraponline opened this issue Oct 26, 2018 · 20 comments
Closed

ktor client iOS hangs forever in runBlocking #678

bootstraponline opened this issue Oct 26, 2018 · 20 comments
Labels

Comments

@bootstraponline
Copy link

bootstraponline commented Oct 26, 2018

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? Kotlin/kotlinx.coroutines#462

@cy6erGn0m
Copy link
Contributor

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

@cy6erGn0m
Copy link
Contributor

Read Guide to UI programming with coroutines first

@cy6erGn0m
Copy link
Contributor

Unfortunately Dispatchers.Main doesn't work with ios yet. See Kotlin/kotlinx.coroutines#470

@bootstraponline
Copy link
Author

bootstraponline commented Oct 26, 2018

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).

@bootstraponline
Copy link
Author

See Kotlin/kotlinx.coroutines#470

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

@cy6erGn0m
Copy link
Contributor

cy6erGn0m commented Oct 26, 2018

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?

@bootstraponline
Copy link
Author

bootstraponline commented Oct 26, 2018

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.

@cy6erGn0m
Copy link
Contributor

cy6erGn0m commented Oct 26, 2018

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

@bootstraponline
Copy link
Author

I pushed some fixes:

anything else obviously wrong?

@cy6erGn0m
Copy link
Contributor

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.

@cy6erGn0m
Copy link
Contributor

Why can't you simply make getGitHub suspend?

@cy6erGn0m
Copy link
Contributor

cy6erGn0m commented Oct 26, 2018

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

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

@bootstraponline
Copy link
Author

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.

@bootstraponline
Copy link
Author

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?

@bootstraponline
Copy link
Author

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

JetBrains/kotlin-native#1684

@cy6erGn0m
Copy link
Contributor

cy6erGn0m commented Oct 26, 2018

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:https://....")
            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

@bootstraponline
Copy link
Author

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.

@bootstraponline
Copy link
Author

bootstraponline commented Oct 27, 2018

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.

@bootstraponline
Copy link
Author

bootstraponline commented Oct 29, 2018

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

bootstraponline/run_blocking@a795319#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()
        }
    }
}

@e5l
Copy link
Member

e5l commented Oct 29, 2018

Thanks for the idea :)

jan-goral added a commit to jan-goral/run_blocking that referenced this issue Feb 4, 2020
Freezes anyway as described here ktorio/ktor#678 (comment)

* Upgrade kotlin coroutines to 1.3.3
* Remove default comments from iOS sources
* Fix package name clash between common and android
bootstraponline pushed a commit to bootstraponline/run_blocking that referenced this issue Feb 4, 2020
Freezes anyway as described here ktorio/ktor#678 (comment)

* Upgrade kotlin coroutines to 1.3.3
* Remove default comments from iOS sources
* Fix package name clash between common and android
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants