Hello guys, I have some calls to a device's SDK, ...
# coroutines
s
Hello guys, I have some calls to a device's SDK, that can make the calling thread be stuck forever. This happens when I forcefully stop the Android service that facilitates the library's functionality (I emulate OS's behavior of killing the service sometimes). To make my sure my wrapper over the SDK always returns and never hangs forever, I've worked around coroutines and timeouts. I think I've finally managed to get it work to always return or throw or exception when accessing the SDK by creating a new single threaded coroutine context, that is created/destroyed when my app is connected/disconnected to the SDK's service.
Copy code
private var sdkCoroutineContext: CoroutineContext? = null

/**
* Manages the [sdkCoroutineContext]
*/
fun connect() {
    sdk
        .bindSdkService(context)
        .doOnNext { event ->
            log.d("bindSdkService event: $event")
            if (event is SdkConnectedEvent) {
                // create SDK coroutine context
                sdkCoroutineContext = newSingleThreadContext("sdk ${event.hashCode()}") + Job()
            } else if (event is SdkDisconnectedEvent) {
                // cancel SDK coroutine context
                sdkCoroutineContext?.cancel(CancellationException("SDK disconnected"))
                sdkCoroutineContext = null
            }
        }
        .subscribe()
}

fun accessSdk(action: Sdk.() -> T): T {
    return try {
        runBlocking {
            withTimeout(timeout) {
                sdkCoroutineContext?.let { context ->
                    val deferred = async(context) {
                        log.d("action starting")
                        action.invoke(sdk).also {
                            log.d("action finished")
                        }
                    }
                    deferred.await()
                }
            }
        } ?: throw SdkNotConnected.also {
            log.e("SDK is not connected")
        }
    } catch (e: CancellationException) {
        log.e(e, "Action hasn't been processed in time")
        throw SdkTimeoutException
    } catch (e: InterruptedException) {
        log.e(e, "runBlocking was interrupted when waiting for SDK action")
        throw SdkTimeoutException
    }
}
This way if the library code hangs I receive
CancellationException
. Even though the
SdkDisconnectedEvent
event is emitted only rarely, I understand that even though I call
sdkCoroutineContext.cancel()
the thread won't stop since it is non-cooperative (it may be stuck somewhere inside the library). Are there any obvious flaws with this approach? Such as expensive synchronization between threads or too many resources leaking due to the library's code being non-cooperative. Thank you very much.
e
in general you should use https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-interruptible.html around blocking Java code that is able to be cancelled by thread interruption. can't tell if that's the case here, though
on the other hand, https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html should never be used by library code, because it can behave unexpectedly if called from inside a coroutine (and a library can't guarantee that its callers aren't)
s
Wow, thanks for mentioning
runInterruptible
as it works exactly as I need - you just saved me the whole thing with another coroutine context managed by a separate thread 😄
We unfortunately have a lot of RxJava interop, and because of that I have to settle in some places to using
runBlocking
:/
I see there is this: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-rx2/ I'll have a look at that 🤔
So I've settled on getting rid of the timeout altogether:
Copy code
private var sdkCoroutineContext: CoroutineContext? = null

    fun <T> useSdk(action: Sdk.() -> T): T =
        try {
            runBlocking {
                sdkCoroutineContext?.let { coroutineContext ->
                    withContext(coroutineContext) {
                        runInterruptible {
                            action.invoke(sdk)
                        }
                    }
                } ?: throw SdkNotConnected.also {
                    log.e("SDK is not connected")
                }
            }
        } catch (e: CancellationException) {
            log.e(e, "SDK action was cancelled")
            throw SdkNotConnected
        } catch (e: InterruptedException) {
            log.e(e, "runBlocking was interrupted when waiting for SDK action")
            throw SdkNotConnected
        }
When the SDK is disconnected and my code gets notified, the
sdkCoroutineContext
is cancelled, and
useSdk
returns immediately.
u
how about using rxSingle instead of runBlocking?