I have found very strange documentation for join method:
In particular, it means that a parent coroutine invoking join on a
child coroutine that was started using launch(coroutineContext) { ...
} builder throws CancellationException if the child had crashed,
unless a non-standard CoroutineExceptionHandler is installed in the
context.
I'm not sure that CoroutineExceptionHandler will have effect for CancellationException.
Example:
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
val inner = launch { // all this stack of coroutines will get cancelled
throw IOException() // the original exception
}
try {
inner.join()
} catch (e: CancellationException) {
println("handle join")
}
}
job.join()
}
Output:
handle
join CoroutineExceptionHandler got java.io.IOException
So basically CancellationException will still be thrown regardless any installed handlers.
Am I right?
Yes. Your innerJob will still throw a CancellationException, and in your outer job you'll get a crash, since you don't handle the exception.
See: https://pl.kotl.in/1Uqw8nmNS
Related
(Kotlin 1.5.21, kotlinx-coroutines-test 1.5.0)
Please consider the following code inside a androidx.lifecycle.ViewModel:
fun mayThrow(){
val handler = CoroutineExceptionHandler { _, t -> throw t }
vmScope.launch(dispatchers.IO + handler) {
val foo = bar() ?: throw IllegalStateException("oops")
withContext(dispatchers.Main) {
_someLiveData.value = foo
}
}
}
vmScope corresponds to viewModelScope, in tests it is replaced by a TestCoroutineScope. The dispatchers.IO is a proxy to Dispatchers.IO, in tests it is a TestCoroutineDispatcher. In this case, the app's behavior is undefined if bar() returns null, so I want it to crash if that's the case. Now I'm trying to (JUnit4) test this code:
#Test(expected = IllegalStateException::class)
fun `should crash if something goes wrong with bar`() {
tested.mayThrow()
}
The test fails because of the very same exception it is supposed to test for:
Exception in thread "Test worker #coroutine#1" java.lang.IllegalStateException: oops
// stack trace
Expected exception: java.lang.IllegalStateException
java.lang.AssertionError: Expected exception: java.lang.IllegalStateException
// stack trace
I have the feeling I'm missing something quite obvious here... Question: is the code in my ViewModel the right way to throw an exception from a coroutine and if yes, how can I unit test it?
If nothing else works I can suggest to move the code, which throws an exception, to another method and test this method:
// ViewModel
fun mayThrow(){
vmScope.launch(dispatchers.IO) {
val foo = doWorkThatThrows()
withContext(dispatchers.Main) {
_someLiveData.value = foo
}
}
}
fun doWorkThatThrows(): Foo {
val foo = bar() ?: throw IllegalStateException("oops")
return foo
}
// Test
#Test(expected = IllegalStateException::class)
fun `should crash if something goes wrong with bar`() {
tested.doWorkThatThrows()
}
Or using JUnit Jupiter allows to test throwing Exceptions by using assertThrows method. Example:
assertThrows<IllegalStateException> { tested.doWorkThatThrows() }
Why the test is green:
code in launch{ ... } is beeing executed asynchronously with the
test method. To recognize it try to modify mayThrow method (see code
snippet below), so it returns a result disregarding of what is going
on inside launch {...} To make the test red replace launch with
runBlocking (more details in docs, just read the first chapter
and run the examples)
#Test
fun test() {
assertEquals(1, mayThrow()) // GREEN
}
fun mayThrow(): Int {
val handler = CoroutineExceptionHandler { _, t -> throw t }
vmScope.launch(dispatchers.IO + handler) {
val foo = bar() ?: throw IllegalStateException("oops")
withContext(dispatchers.Main) {
_someLiveData.value = foo
}
}
return 1 // this line succesfully reached
}
Why it looks like "test fails because of the very same exception ..."
the test does not fail, but we see the exception stacktrace in console, because
the default exception handler works so and it is applied, because in this case the custom exception handler CoroutineExceptionHandler throws (detailed explanation)
How to test
Function mayThrow has too many responsibilities, that is why it is hard to test. It is a standard problem and there are standard treatments (first, second): long story short is apply Single responsibility principle.
For instance, pass exception handler to the function
fun mayThrow(xHandler: CoroutineExceptionHandler){
vmScope.launch(dispatchers.IO + xHandler) {
val foo = bar() ?: throw IllegalStateException("oops")
withContext(dispatchers.Main) {
_someLiveData.value = foo
}
}
}
#Test(expected = IllegalStateException::class)
fun test() {
val xRef = AtomicReference<Throwable>()
mayThrow(CoroutineExceptionHandler { _, t -> xRef.set(t) })
val expectedTimeOfAsyncLaunchMillis = 1234L
Thread.sleep(expectedTimeOfAsyncLaunchMillis)
throw xRef.get() // or assert it any other way
}
In my sample I'm calling network operation and emitting success case but on error e.g 404 app crashes wihout emitting exception. Surrendering with try catch prevent crashes but I want to pass error till the ui layer like success case.
suspend fun execute(
params: Params,
):
Flow<Result<Type>> = withContext(Dispatchers.IO) {
flow {
emit(Result.success(run(params)))
}.catch {
emit(Result.failure(it))
}
}
There is a helpful function runCatching for creating a Result easily, but the problem in coroutines is that you don't want to be swallowing CancellationExceptions. So below, I'm using runCatchingCancellable from my answer here.
This shouldn't be a Flow since it returns a single item.
If run is a not a blocking function (it shouldn't be if you are using Retrofit with suspend functions), your code can simply be:
suspend fun execute(params: Params): Result<Type> = runCatchingCancellable {
run(params)
}
If it is a blocking function you can use:
suspend fun execute(params: Params): Result<Type> = runCatchingCancellable {
withContext(Dispatchers.IO) {
run(params)
}
}
If you were going to return a Flow (which you shouldn't for a returning a single item!!), then you shouldn't make this a suspend function, and you should catch the error inside the flow builder lambda:
fun execute(params: Params): Flow<Result<Type>> = flow {
emit(runCatchingCancellable {
run(params)
})
}
// or if run is blocking (it shouldn't be):
fun execute(params: Params): Flow<Result<Type>> = flow {
emit(runCatchingCancellable {
withContext(Dispatchers.IO) { run(params) }
})
}
If you want to use flows you can use the catch method of flows.
As you said you can use try-catch but it would break the structured concurrency since it would catch the cancellation exception as well or it would avoid the cancellation exception to be thrown.
One thing that you can do is to use an Exception handler at the point where you launch the root coroutine that calls the suspend function.
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
// handle it
}
scope.launch(handler) { // root coroutine
execute(params)
somethingThatShouldBeExecutedOnlyIfPreviousCallDoesNotThrow()
}
This solution is good for both flows and non-flow coroutines.
In the solution with the runCatching you will have to manually check the result of the first execute to avoid the second one to run.
One interesting thread is here.
I am trying to run a test in which I want to wait till higher order function executes. As of now I am not able to figure out any ways to do it. Following is my code.
#Test
fun `test execute routine error`() = runBlocking(coroutineDispatcher) {
val observer = mock<Observer<String>>()
baseViewModel.error.observeForever(observer)
val httpException = HttpException(Response.error<String>(402, mock(ResponseBody::class.java)))
val function = baseViewModel.executeRoutine {
throw httpException
}
verify(observer).onChanged("Something went wrong. Please try again")
}
The problem with above snippet is that it jumps to the last line i.e. verify() before throwing an http exception for executeRoutine.
Update: Execute routine definition
fun executeRoutine(requestType: RequestType = RequestType.POST_LOGIN, execute: suspend () -> Unit) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
_spinner.postValue(true)
try {
execute()
} catch (ex: HttpException) {
val errorHandler = errorHandlerFactory.create(requestType)
_error.postValue(errorHandler.getErrorMessageFrom(ex))
} catch (ex: Exception) {
_error.postValue(ex.localizedMessage)
Timber.e(ex)
} finally {
_spinner.postValue(false)
}
}
}
}
The problem is that the higher order function does execute, it just doesn't do what you think it does -- its execution is launching the task, not waiting for it to complete.
You will have to solve the problem another way, by either having your test wait until the change is observed, or having the callback complete a barrier to allow the test to proceed (e.g. completableJob.complete() at the end of the call back, and completableJob.join() waiting before proceeding with the test).
It might also be desirable to rearchitect your code so you don't have to do anything special, e.g. by making executeRoutine a suspend function executing the code rather than launching the code in another scope.
I'm still new to coroutines (but already love them lots). I've got this error (and crash) and not sure what it means. What is an UndispatchedCoroutine? how it gets cancelled?
kotlinx.coroutines.JobCancellationException: UndispatchedCoroutine was cancelled; job="coroutine#8":UndispatchedCoroutine{Cancelled}#fe7d397
Edit:
To give some context, I'm using an Actor to execute the handling of the message in a coroutine:
#OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class)
internal inner class HandlerFPMsg : Handler() {
private val msgActor = coroutineScope.actor<Pair<Int, Long>>(Dispatchers.Default, capacity = Channel.UNLIMITED) {
for(msg in channel)
handleMessageWorker(msg.first, msg.second)
}
override fun handleMessage(msg: Message) {
msgActor.offer(Pair(msg.what, msg.data.getLong("ID",-1L)))
super.handleMessage(msg)
}
private suspend fun handleMessageWorker(what: Int, id: Long) {
if (what != 0x92) Log.d("Messenger123", "Message Received: [" + what.toString(16) + "]")
when (what) {
MsgConstants.MSG_CONSTANT1 -> {
someFunction()
Log.e(TAG, "${e.message}")
}
MsgConstants.MSG_CONSTANT2 -> {
if (id != lastId) return
// -----------------------
if (aSuspendingFunction()) {
msgService?.let { msgr -> aClass.sendAmsg(msgr) }
} else {
// some comment
someFunction()
}
}
}
}
}
if i wrap handleMessageWorker(msg.first, msg.second) with a try catch block seems to work (but i don't know how correct would be the code...)
This is not a definitive answer, but I find that comments are too short for that.
Judging by the fact that this is an inner class, and that you're creating your actor using coroutineScope from the outside, my guess would be that your coroutineScope gets terminated, which also terminates your actor. This is the correct behavior of structured concurrency.
Question is: why your coroutine scope gets terminated.
Two main possibilities:
It's bound to lifecycle of another object. If you're launching this actor from scope of an Android Activity, coroutineScope may get terminated once the activity closes.
Another piece of code that uses the same scope throws an exception. This will also cause scope to terminate, unless it's supervisorScope
I have the following code structure:
#Throws(InterruptedException::class)
fun method() {
// do some blocking operations like Thread.sleep(...)
}
var job = launch {
method()
}
job.cancelAndJoin()
The method is provided by the external library and I can't control its behaviour. It can take a lot of time for execution, so in some cases it should be canceled by timeout.
I can use the withTimeout function provided by the kotlin coroutines library, but it can't cancel a code with blockings due to the coroutines design. It there some workaround to do it?
The main idea is to use the out of coroutines context thread pool with JVM threads that can be interrupted in the old style and subscribe to the cancellation event from the coroutine execution. When the event is caught by invokeOnCancellation, we can interrupt the current thread.
The implementation:
val externalThreadPool = Executors.newCachedThreadPool()
suspend fun <T> withTimeoutOrInterrupt(timeMillis: Long, block: () -> T) {
withTimeout(timeMillis) {
suspendCancellableCoroutine<Unit> { cont ->
val future = externalThreadPool.submit {
try {
block()
cont.resumeWith(Result.success(Unit))
} catch (e: InterruptedException) {
cont.resumeWithException(CancellationException())
} catch (e: Throwable) {
cont.resumeWithException(e);
}
}
cont.invokeOnCancellation {
future.cancel(true)
}
}
}
}
It provides a similar behaviour like usual withTimeout, but it additionally supports running a code with blockings.
Note: It should be called only when you know, that the inner code use blockings and can correctly process a thrown InterruptedException. In most cases, the withTimeout function is preferred.
UPDATE: Since the coroutines version 1.3.7, there has been a new function runInterruptible, which provides the same behaviour. So this code can be simplified:
suspend fun <T> withTimeoutOrInterrupt(timeMillis: Long, block: () -> T) {
withTimeout(timeMillis) {
runInterruptible(Dispatchers.IO) {
block()
}
}
}