-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Negative consequences of catchNonTestRelatedExceptions from TestScope to Spring Boot apps #4335
Comments
@man85, that's intentional, because we don't publicly support this functionality. It's purely a workaround for some codebases that have their own specialized error handling or a temporary measure for large codebases migrating to a new version of If you do want to change that value anyway, here's how: #3736 (comment) Could you explain why you want to do that? We are interested in learning the use cases to decide if this needs a proper public API, a hidden API for workarounds, or if it can be removed. |
We have a test class which makes database migrations before each test method via JUnit extension and it does it with a SupervisorJob. In this job happens exception, because of wrong SQL for the first test method (with runTest) and this exception is being processed with In this case, such an exception in a job coroutine should not be treated as an error and should not be collected by the ExceptionCollector. That's why I would like to disable this feature of collecting unprocessed errors. Same case was described here. |
That's not the intended way to process exceptions. If you check your logs, you'll probably see that these failures are spammed as stacktraces. What you need is https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler . With |
If you are trying to replace
To answer your question directly, you need to let the service loader machinery know about the exposed service, so you need to add a META-INF file like https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/kotlinx-coroutines-android/resources/META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler |
Thanks for the clarification! |
This is how |
If test classes are executed in parallel, is it possible that an exception from one test class will be reported in another? |
But Try execute:
@dkhalanskyjb Should I report it as a bug? |
Yes, it is possible. Steps to reproduce:
@dkhalanskyjb Should I report it as a bug? |
Yes, that's true. We intentionally chose to do this so that there wouldn't be a memory leak due to storing exceptions for a
I don't see the point. First, it's impossible for us to fix this short of prohibiting parallel |
I can't agree with you.
|
They are not. As I said in #4335 (comment), val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job)
scope.launch {
throw IllegalStateException("never happens")
}.invokeOnCompletion {
if (it == null) {
println("No exception")
} else {
println("Got an exception ($it)")
}
}
Thread.sleep(100) This code finishes without errors, but in the console, we see: Exception in thread "DefaultDispatcher-worker-1 @coroutine#1" java.lang.IllegalStateException: never happens
at kotlinx.coroutines.C$failure$1.invokeSuspend(DispatcherKeyTest.kt:59)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:829)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [CoroutineId(1), "coroutine#1":StandaloneCoroutine{Cancelling}@1d9cabc0, Dispatchers.Default]
Got an exception (java.lang.IllegalStateException: never happens) On Native, the equivalent code crashes (replace Execution failed for task ':kotlinx-coroutines-core:linuxX64Test'.
> Test running process exited unexpectedly.
Current test: failure
Process output:
at 0 test.kexe 0xb64d0d kfun:kotlin.Throwable#<init>(kotlin.String?){} + 93
at 1 test.kexe 0xb5ffa9 kfun:kotlin.Exception#<init>(kotlin.String?){} + 89
at 2 test.kexe 0xb600e9 kfun:kotlin.RuntimeException#<init>(kotlin.String?){} + 89
at 3 test.kexe 0xb603f9 kfun:kotlin.IllegalStateException#<init>(kotlin.String?){} + 89
at 4 test.kexe 0xb34138 kfun:kotlinx.coroutines.C.failure$lambda$0#internal + 152
at 5 test.kexe 0xb3437d kfun:kotlinx.coroutines.C.$failure$lambda$0$FUNCTION_REFERENCE$9.invoke#internal + 109
at 6 test.kexe 0xc75b78 kfun:kotlin.Function2#invoke(1:0;1:1){}1:2-trampoline + 88
at 7 test.kexe 0xb6eeb6 kfun:kotlin.coroutines.intrinsics.object-4.invokeSuspend#internal + 1158
at 8 test.kexe 0xc754f3 kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any?-trampoline + 51
at 9 test.kexe 0xb69c13 kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 963
at 10 test.kexe 0xc75599 kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){}-trampoline + 73
at 11 test.kexe 0x493209 kfun:kotlinx.coroutines.DispatchedTask#run(){} + 2969
at 12 test.kexe 0xb3c5f0 kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 64
at 13 test.kexe 0x4c44f2 kfun:kotlinx.coroutines.MultiWorkerDispatcher.$workerRunLoop$lambda$2COROUTINE$0.invokeSuspend#internal + 2066
at 14 test.kexe 0x4c4a42 kfun:kotlinx.coroutines.MultiWorkerDispatcher.workerRunLoop$lambda$2#internal + 322
at 15 test.kexe 0x4c4d93 kfun:kotlinx.coroutines.MultiWorkerDispatcher.$workerRunLoop$lambda$2$FUNCTION_REFERENCE$4.invoke#internal + 131
at 16 test.kexe 0xc75b78 kfun:kotlin.Function2#invoke(1:0;1:1){}1:2-trampoline + 88
at 17 test.kexe 0xb6eeb6 kfun:kotlin.coroutines.intrinsics.object-4.invokeSuspend#internal + 1158
at 18 test.kexe 0xc754f3 kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any?-trampoline + 51
at 19 test.kexe 0xb69c13 kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 963
at 20 test.kexe 0xc75599 kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){}-trampoline + 73
at 21 test.kexe 0x493209 kfun:kotlinx.coroutines.DispatchedTask#run(){} + 2969
at 22 test.kexe 0xb3c5f0 kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 64
at 23 test.kexe 0x3c3a8b kfun:kotlinx.coroutines.EventLoopImplBase#processNextEvent(){}kotlin.Long + 219
at 24 test.kexe 0xb3c403 kfun:kotlinx.coroutines.EventLoop#processNextEvent(){}kotlin.Long-trampoline + 35
at 25 test.kexe 0x4bbf67 kfun:kotlinx.coroutines.BlockingCoroutine.joinBlocking#internal + 647
at 26 test.kexe 0x4bb75f kfun:kotlinx.coroutines#runBlocking(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>){0§<kotlin.Any?>}0:0 + 1919
at 27 test.kexe 0x4bb984 kfun:kotlinx.coroutines#runBlocking$default(kotlin.coroutines.CoroutineContext?;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>;kotlin.Int){0§<kotlin.Any?>}0:0 + 260
at 28 test.kexe 0x4c1ccb kfun:kotlinx.coroutines.MultiWorkerDispatcher.workerRunLoop#internal + 187
at 29 test.kexe 0x4c384b kfun:kotlinx.coroutines.MultiWorkerDispatcher.<init>$lambda$1$lambda$0#internal + 59
at 30 test.kexe 0x4c4f8f kfun:kotlinx.coroutines.MultiWorkerDispatcher.$<init>$lambda$1$lambda$0$FUNCTION_REFERENCE$6.invoke#internal + 63
at 31 test.kexe 0x4c504f kfun:kotlinx.coroutines.MultiWorkerDispatcher.$<init>$lambda$1$lambda$0$FUNCTION_REFERENCE$6.$<bridge-DNN>invoke(){}#internal + 63
at 32 test.kexe 0xc73718 kfun:kotlin.Function0#invoke(){}1:0-trampoline + 72
at 33 test.kexe 0xb74fe9 WorkerLaunchpad + 105
at 34 test.kexe 0xca4edb _ZN6Worker19processQueueElementEb + 3403
at 35 test.kexe 0xca40c1 _ZN12_GLOBAL__N_113workerRoutineEPv + 177
at 36 libc.so.6 0x7f61f8e2cd01 0x0 + 140058764168449
at 37 libc.so.6 0x7f61f8eac3ab 0x0 + 140058764690347
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@f7cc3340, DefaultDispatcher@f72225c8]
at 0 test.kexe 0xb64d0d kfun:kotlin.Throwable#<init>(kotlin.String?){} + 93
at 1 test.kexe 0xb5ffa9 kfun:kotlin.Exception#<init>(kotlin.String?){} + 89
at 2 test.kexe 0xb600e9 kfun:kotlin.RuntimeException#<init>(kotlin.String?){} + 89
at 3 test.kexe 0x4c64c7 kfun:kotlinx.coroutines.internal.DiagnosticCoroutineContextException#<init>(kotlin.coroutines.CoroutineContext){} + 167
at 4 test.kexe 0x48e3ca kfun:kotlinx.coroutines.internal#handleUncaughtCoroutineException(kotlin.coroutines.CoroutineContext;kotlin.Throwable){} + 874
at 5 test.kexe 0x3bee20 kfun:kotlinx.coroutines#handleCoroutineException(kotlin.coroutines.CoroutineContext;kotlin.Throwable){} + 944
at 6 test.kexe 0x3b0ddf kfun:kotlinx.coroutines.StandaloneCoroutine.handleJobException#internal + 159
at 7 test.kexe 0xb3d3bb kfun:kotlinx.coroutines.JobSupport#handleJobException(kotlin.Throwable){}kotlin.Boolean-trampoline + 43
at 8 test.kexe 0x3cb28e kfun:kotlinx.coroutines.JobSupport.finalizeFinishingState#internal + 1582
at 9 test.kexe 0x3d57c6 kfun:kotlinx.coroutines.JobSupport.tryMakeCompletingSlowPath#internal + 3062
at 10 test.kexe 0x3d4b89 kfun:kotlinx.coroutines.JobSupport.tryMakeCompleting#internal + 697
at 11 test.kexe 0x3d4670 kfun:kotlinx.coroutines.JobSupport#makeCompletingOnce(kotlin.Any?){}kotlin.Any? + 672
at 12 test.kexe 0x3abc64 kfun:kotlinx.coroutines.AbstractCoroutine#resumeWith(kotlin.Result<1:0>){} + 228
at 13 test.kexe 0xc75599 kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){}-trampoline + 73
at 14 test.kexe 0xb69efc kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 1708
... and 28 more common stack frames skipped
Uncaught Kotlin exception: So, you are not normally processing exceptions. This code does process exceptions: val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job + CoroutineExceptionHandler {
_, throwable -> println("Caught an exception ($throwable)")
})
scope.launch {
throw IllegalStateException("never happens")
} It won't crash. This is fine as well: val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job)
scope.launch(CoroutineExceptionHandler {
_, throwable -> println("Caught an exception ($throwable)")
}) {
throw IllegalStateException("never happens")
} |
This means that uncaught exceptions are normal when a coroutine is started from a non-suspended function. import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.junit.jupiter.api.Test
class ExceptionInsideCoroutineTest {
@Test
fun exceptionInsideSupervisorJob() {
val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job)
scope.launch {
throw IllegalStateException("never happens")
}.invokeOnCompletion {
if (it == null) {
println("No exception")
} else {
println("Got an exception ($it)")
}
}
Thread.sleep(1000)
}
}
@dkhalanskyjb You should not care about the console. This situation is like you do some work in daemon thread, but if this daemon thread fails, the app should not fail.
Native is not a common case |
Android also crashes the whole application if there are uncaught exceptions. If something can be called the common case for Kotlin, it's Android.
On the contrary, you should. If you are handling coroutine exceptions properly, then the only exceptions in your logs are exceptions you don't expect and don't properly handle, which usually indicates a problem in your code. It's very convenient when the console helps you debug your code.
The guiding principle for exception handling in Kotlin is https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07, which says that no, they aren't. Because of that, Also, there are no reasons to use
Many typical cases of exception handling don't have these properties, which is why |
@dkhalanskyjb Can you give an example of such crash in JVM? |
Android is JVM, and the exact code example you provide will crash the
application. For non-JVM Android, there typically won't be a crash.
…On Tue, Jan 28, 2025, 7:41 PM man85 ***@***.***> wrote:
Android also crashes the whole application if there are uncaught exceptions
@dkhalanskyjb <https://github.com/dkhalanskyjb> Can you give an example
of such crash in JVM?
—
Reply to this email directly, view it on GitHub
<#4335 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AMT73TPBPJUDKKQ7WGYHU6L2M7FNJAVCNFSM6AAAAABVLKO27CVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMMJZG44TCMZUGY>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
@dkhalanskyjb Android is a special case. Could you give an example of Spring Boot application with such crash? |
Nope, I think a Spring Boot application wouldn't crash because of incorrect handling of uncaught exceptions in coroutines. |
As a summary:
|
This part of your summary is incorrect. Tests very often fail when production would keep working, but produce the wrong result or have undesirable aspects of behavior. That's what test assertions are for. In your scenario, the test failure warns you about the undesirable behavior of invoking the default thread exception handler.
If we see a compelling use case for that, sure, we're ready to expose this functionality. The code examples you've shown in this thread are all incorrectly handling coroutine failures, and fixing that gets rid of the issue, cleans up your logs, improves the performance in cases when there are many failing coroutines, makes your code more idiomatic, and helps you avoid the error-prone |
@dkhalanskyjb , what is the correct way to do some logic asynchronously after a coroutine job has successfully completed? |
There are several options, depending on what behavior you want. Here are a couple of examples, but if you clarify your use case (what should happen in the cleanup when your job is cancelled, fails with an error, or succeeds; which thread/dispatcher should run the logic after the completion of the coroutine; also, whether you control the code running in the coroutine, whether you have access to the scope in which that coroutine is launched, and whether you control if it's The easiest and most idiomatic solution is to use val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
try {
// what you want to do
} catch (e: IllegalArgumentException) { // or any other exception you're expecting
// process the exceptions
} finally {
// cleanup resources
}
} Alternatively, you can replace val scope = CoroutineScope(Dispatchers.Default)
val anotherScope = CoroutineScope(Dispatchers.Default)
val deferred = scope.async { /* some code */ }
anotherScope.launch {
try {
deferred.await()
// your normal non-throwing cleanup code
} catch (e: Throwable) {
// your cleanup code for the error scenario
}
} In the example above, I chose Or another scenario: val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch(CoroutineExceptionHandler { ctx, throwable -> /* process the exception */ }) {
/* some code */
}
scope.launch { // will not run at all if `scope` fails before this code block is entered; use CoroutineStart.ATOMIC to change this
job.join() // await completion, or fail with a `CancellationException` if `scope` gets cancelled
// cleanup for normal completion
} I could go on, because there are many different use cases that require different threading and error propagation behavior, but in general, the approach is: launch a new coroutine that waits for your coroutine to complete, and there, process the result as needed. |
There is no ability to change the value of catchNonTestRelatedExceptions, but kdoc says "some tests may want to disable it" @dkhalanskyjb
The text was updated successfully, but these errors were encountered: