Skip to content

Commit 8f3aa33

Browse files
author
Anton Keks, Margo Mitt
committed
improve signature of launchContext
Reasons: 1. Extra coroutine context often needs to access Jooby Context 2. Relying on users to extend the default CoroutineContext can be error prone - users can forget to include the ExceptionHandler, resuling in "hanging" asynchronous requests
1 parent 3ac30aa commit 8f3aa33

File tree

4 files changed

+31
-31
lines changed

4 files changed

+31
-31
lines changed

jooby/src/main/kotlin/io/jooby/CoroutineRouter.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import kotlinx.coroutines.CoroutineStart
1919
import kotlinx.coroutines.asCoroutineDispatcher
2020
import kotlinx.coroutines.launch
2121
import kotlin.coroutines.CoroutineContext
22+
import kotlin.coroutines.EmptyCoroutineContext
2223

2324
internal class RouterCoroutineScope(override val coroutineContext: CoroutineContext) : CoroutineScope
2425

@@ -28,9 +29,9 @@ class CoroutineRouter(val coroutineStart: CoroutineStart, val router: Router) {
2829
RouterCoroutineScope(router.worker.asCoroutineDispatcher())
2930
}
3031

31-
private var extendCoroutineContext: (CoroutineContext) -> CoroutineContext = { it }
32-
fun launchContext(block: (CoroutineContext) -> CoroutineContext) {
33-
extendCoroutineContext = block
32+
private var extraCoroutineContextProvider: HandlerContext.() -> CoroutineContext = { EmptyCoroutineContext }
33+
fun launchContext(provider: HandlerContext.() -> CoroutineContext) {
34+
extraCoroutineContextProvider = provider
3435
}
3536

3637
@RouterDsl
@@ -67,16 +68,18 @@ class CoroutineRouter(val coroutineStart: CoroutineStart, val router: Router) {
6768

6869
fun route(method: String, pattern: String, handler: suspend HandlerContext.() -> Any): Route =
6970
router.route(method, pattern) { ctx ->
70-
launch(ctx) {
71-
val result = handler(HandlerContext(ctx))
71+
val handlerContext = HandlerContext(ctx)
72+
launch(handlerContext) {
73+
val result = handler(handlerContext)
7274
if (result != ctx) {
7375
ctx.render(result)
7476
}
7577
}
7678
}.setHandle(handler).attribute("coroutine", true)
7779

78-
internal fun launch(ctx: Context, block: suspend CoroutineScope.() -> Unit) {
79-
val exceptionHandler = CoroutineExceptionHandler { _, x -> ctx.sendError(x) }
80-
coroutineScope.launch(extendCoroutineContext(exceptionHandler), coroutineStart, block)
80+
internal fun launch(handlerContext: HandlerContext, block: suspend CoroutineScope.() -> Unit) {
81+
val exceptionHandler = CoroutineExceptionHandler { _, x -> handlerContext.ctx.sendError(x) }
82+
val coroutineContext = exceptionHandler + handlerContext.extraCoroutineContextProvider()
83+
coroutineScope.launch(coroutineContext, coroutineStart, block)
8184
}
8285
}

jooby/src/main/kotlin/io/jooby/HandlerContext.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ class AfterContext(val ctx: Context, val result: Any?, val failure: Any?)
99

1010
class DecoratorContext(val ctx: Context, val next: Route.Handler)
1111

12-
class HandlerContext (val ctx: Context)
12+
class HandlerContext(val ctx: Context)
1313

14-
class WebSocketInitContext (val ctx: Context, val configurer: WebSocketConfigurer)
14+
class WebSocketInitContext(val ctx: Context, val configurer: WebSocketConfigurer)
1515

16-
class ServerSentHandler (val ctx: Context, val sse: ServerSentEmitter)
16+
class ServerSentHandler(val ctx: Context, val sse: ServerSentEmitter)

jooby/src/main/kotlin/io/jooby/internal/mvc/CoroutineLauncher.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package io.jooby.internal.mvc
77

88
import io.jooby.Context
99
import io.jooby.CoroutineRouter
10+
import io.jooby.HandlerContext
1011
import io.jooby.Route
1112
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
1213

@@ -16,7 +17,7 @@ import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
1617
class CoroutineLauncher(val next: Route.Handler) : Route.Handler {
1718
override fun apply(ctx: Context) = ctx.also {
1819
val router = ctx.router.attribute<CoroutineRouter>("coroutineRouter")
19-
router.launch(ctx) {
20+
router.launch(HandlerContext(ctx)) {
2021
val result = suspendCoroutineUninterceptedOrReturn<Any> {
2122
ctx.attribute("___continuation", it)
2223
next.apply(ctx)
Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
package io.jooby
22

33
import io.jooby.Router.GET
4-
import kotlinx.coroutines.CoroutineExceptionHandler
54
import kotlinx.coroutines.CoroutineStart
5+
import kotlinx.coroutines.coroutineScope
6+
import org.junit.jupiter.api.Assertions.assertSame
7+
import org.junit.jupiter.api.Assertions.assertTrue
68
import org.junit.jupiter.api.Test
79
import org.mockito.ArgumentCaptor
810
import org.mockito.Mockito.RETURNS_DEEP_STUBS
9-
import org.mockito.Mockito.`when`
10-
import org.mockito.Mockito.any
11-
import org.mockito.Mockito.argThat
1211
import org.mockito.Mockito.eq
1312
import org.mockito.Mockito.mock
1413
import org.mockito.Mockito.verify
15-
import org.mockito.Mockito.verifyNoInteractions
1614
import kotlin.coroutines.AbstractCoroutineContextElement
1715
import kotlin.coroutines.CoroutineContext
1816

@@ -35,26 +33,24 @@ class CoroutineRouterTest {
3533

3634
@Test
3735
fun launchContext_isRunEveryTime() {
38-
val mockCoroutineContext = mock(CoroutineContext::class.java)
39-
`when`(mockCoroutineContext.plus(any()
40-
?: mockCoroutineContext)).thenReturn(mockCoroutineContext, ExtraContext())
41-
42-
CoroutineRouter(CoroutineStart.DEFAULT, router).apply {
43-
launchContext { mockCoroutineContext + it + ExtraContext() }
44-
get("/path") { "Result" }
36+
var coroutineRouteCalled = false
37+
CoroutineRouter(CoroutineStart.UNDISPATCHED, router).apply {
38+
launchContext { SampleCoroutineContext(ctx) }
39+
get("/path") {
40+
coroutineScope {
41+
assertSame(ctx, coroutineContext[SampleCoroutineContext.Key]!!.ctx)
42+
coroutineRouteCalled = true
43+
}
44+
}
4545
}
4646

4747
val handlerCaptor = ArgumentCaptor.forClass(Route.Handler::class.java)
4848
verify(router).route(eq(GET), eq("/path"), handlerCaptor.capture())
49-
verifyNoInteractions(mockCoroutineContext)
50-
5149
handlerCaptor.value.apply(ctx)
52-
verify(mockCoroutineContext).plus(argThat { it is CoroutineExceptionHandler }
53-
?: mockCoroutineContext)
54-
verify(mockCoroutineContext).plus(argThat { it is ExtraContext } ?: mockCoroutineContext)
50+
assertTrue(coroutineRouteCalled)
5551
}
5652

57-
class ExtraContext : AbstractCoroutineContextElement(Key) {
58-
companion object Key : CoroutineContext.Key<ExtraContext>
53+
class SampleCoroutineContext(val ctx: Context) : AbstractCoroutineContextElement(Key) {
54+
companion object Key : CoroutineContext.Key<SampleCoroutineContext>
5955
}
6056
}

0 commit comments

Comments
 (0)