Tóth István Zoltán
07/22/2024, 8:47 AMcancel()
. I'm trying to write client-server unit tests and I would like to properly clean up at the end of each case. However, if I do not add a delay(1000)
after cancel()
, I get an exception like java.util.concurrent.RejectedExecutionException: event executor terminated
(1000 in the delay is not really important 100 works just as well).gildor
07/22/2024, 8:53 AMgildor
07/22/2024, 8:53 AMgildor
07/22/2024, 8:54 AMTóth István Zoltán
07/22/2024, 8:58 AMCoroutineScope
has only cancel
as I see.
The full stack trace:
Exception in thread "DefaultDispatcher-worker-4 @ws-writer#20" kotlinx.coroutines.CompletionHandlerException: Exception in completion handler ChildCompletion@1bda3618[job@5b137bda] for "ws-writer#20":StandaloneCoroutine{Cancelled}@5b137bda
at kotlinx.coroutines.JobSupport.notifyCompletion(JobSupport.kt:1502)
at kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:325)
at kotlinx.coroutines.JobSupport.finalizeFinishingState(JobSupport.kt:242)
at kotlinx.coroutines.JobSupport.tryMakeCompletingSlowPath(JobSupport.kt:910)
at kotlinx.coroutines.JobSupport.tryMakeCompleting(JobSupport.kt:867)
at kotlinx.coroutines.JobSupport.makeCompletingOnce$kotlinx_coroutines_core(JobSupport.kt:832)
at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:100)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:234)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [CoroutineName(ws-writer), CoroutineId(20), "ws-writer#20":StandaloneCoroutine{Cancelled}@5b137bda, io.ktor.server.netty.EventLoopGroupProxy@bbd9d2]
Caused by: kotlinx.coroutines.CompletionHandlerException: Exception in completion handler ResumeOnCompletion@1f944d25[job@55b2a76e] for JobImpl{Cancelled}@55b2a76e
at kotlinx.coroutines.JobSupport.notifyCompletion(JobSupport.kt:1502)
at kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:325)
at kotlinx.coroutines.JobSupport.finalizeFinishingState(JobSupport.kt:242)
at kotlinx.coroutines.JobSupport.continueCompleting(JobSupport.kt:939)
at kotlinx.coroutines.JobSupport.access$continueCompleting(JobSupport.kt:25)
at kotlinx.coroutines.JobSupport$ChildCompletion.invoke(JobSupport.kt:1159)
at kotlinx.coroutines.JobSupport.notifyCompletion(JobSupport.kt:1497)
... 14 more
Caused by: java.util.concurrent.RejectedExecutionException: event executor terminated
at io.netty.util.concurrent.SingleThreadEventExecutor.reject(SingleThreadEventExecutor.java:934)
at io.netty.util.concurrent.SingleThreadEventExecutor.offerTask(SingleThreadEventExecutor.java:351)
at io.netty.util.concurrent.SingleThreadEventExecutor.addTask(SingleThreadEventExecutor.java:344)
at io.netty.util.concurrent.SingleThreadEventExecutor.execute(SingleThreadEventExecutor.java:836)
at io.netty.util.concurrent.SingleThreadEventExecutor.execute0(SingleThreadEventExecutor.java:827)
at io.netty.util.concurrent.SingleThreadEventExecutor.execute(SingleThreadEventExecutor.java:817)
at io.ktor.server.netty.NettyDispatcher.dispatch(CIO.kt:69)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:161)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497)
at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:368)
at kotlinx.coroutines.ResumeOnCompletion.invoke(JobSupport.kt:1391)
at kotlinx.coroutines.JobSupport.notifyCompletion(JobSupport.kt:1497)
... 20 more
gildor
07/22/2024, 8:59 AMTóth István Zoltán
07/22/2024, 8:59 AMgildor
07/22/2024, 9:02 AMcurrentCoroutineContext()[Job]?.cancelAndJoin()
gildor
07/22/2024, 9:02 AMgildor
07/22/2024, 9:03 AMTóth István Zoltán
07/22/2024, 9:04 AMfun start() {
scope.launch { run() }
scope.launch { timeout() }
}
gildor
07/22/2024, 9:05 AMscope.coroutineContext[Job]?.cancelAndJoin()
Sam
07/22/2024, 9:07 AMcoroutineScope { … }
is likely to be more suitable.Tóth István Zoltán
07/22/2024, 9:07 AMTóth István Zoltán
07/22/2024, 9:08 AMstart
.Sam
07/22/2024, 9:09 AMSam
07/22/2024, 9:09 AMTóth István Zoltán
07/22/2024, 9:09 AMTóth István Zoltán
07/22/2024, 9:10 AMgildor
07/22/2024, 9:58 AMZach Klippenstein (he/him) [MOD]
07/22/2024, 4:45 PMscope.launch {
withTimeout {
run()
}
}
Or if timeout()
is really something special,
scope.launch {
launch { run() }
launch { timeout() }
}
to preserve the call hierarchy in the job hierarchyTóth István Zoltán
07/23/2024, 12:43 AMgildor
07/23/2024, 1:18 AMTóth István Zoltán
07/23/2024, 1:28 AMval scope = CoroutineScope(Dispatchers.Default)
gildor
07/23/2024, 1:32 AMgildor
07/23/2024, 1:34 AMTóth István Zoltán
07/23/2024, 1:37 AMscope.cancel()
? Also, I don't get why delay(100)
solves this issue.gildor
07/23/2024, 1:37 AMfun BasicWebSocketServiceCallTransport.stop() {
runBlocking {
val job = requreNotNull(scope.coroutineContext[Job]) { "Incorrect scope: No Job found" }
job.cancelAndJoin()
}
}
Tóth István Zoltán
07/23/2024, 1:38 AMgildor
07/23/2024, 1:47 AMscope.cancel()
Hard to say exactly, need to understand the whole setup there.
> why delay(100)
solves this issue.
From your code it looks that it just gives time of job to shutdown and blocks thread on which it runs. So it actually mix of coroutines dispatching and thread blocking, which itself may be tricky (for cases when thread on which coroutines is executing is blocked)
In general, try to use coroutines test library is a good idea, but I feel that it way more complex in your case, with global state, which may not be easy to adopt.
But if you look into examples of other ktor tests, it actually not what I would expect, I have quite a few of them in a couple of projects and it never needed to have some global dispatching, essentially just use ktor-server-test-host and testApplication {}
block, which encapsulates test host in a single lambda for each test where I can send and receive requests and if I need some background work, I just use testApplication scope, so coroutine never escapes it
But as I said, I'm not exactly sure what you try to achieve here at the first glancegildor
07/23/2024, 1:51 AMTóth István Zoltán
07/23/2024, 1:54 AMstop
is there because of the delay
workaround. So, there are no nested runBlockings at the end.gildor
07/23/2024, 2:13 AMTóth István Zoltán
07/23/2024, 2:34 AMwithProtoWebSocketTransport
creates a new transport and assigns it to defaultServiceCallTransport
. During normal application startup this typically happens once, but for tests I try to clean up everything.