diff --git a/kotlin/src/main/kotlin/com/linecorp/armeria/server/kotlin/CoroutineHttpService.kt b/kotlin/src/main/kotlin/com/linecorp/armeria/server/kotlin/CoroutineHttpService.kt new file mode 100644 index 00000000000..dbc4e5c0ac0 --- /dev/null +++ b/kotlin/src/main/kotlin/com/linecorp/armeria/server/kotlin/CoroutineHttpService.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.server.kotlin + +import com.linecorp.armeria.common.HttpRequest +import com.linecorp.armeria.common.HttpResponse +import com.linecorp.armeria.common.kotlin.CoroutineContexts +import com.linecorp.armeria.common.kotlin.asCoroutineContext +import com.linecorp.armeria.server.HttpService +import com.linecorp.armeria.server.ServiceRequestContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.future.future +import kotlin.coroutines.EmptyCoroutineContext + +/** + * A Coroutine-based [HttpService] + */ +fun interface CoroutineHttpService : HttpService { + /** + * Calls [suspendedServe] in a [CoroutineScope] and returns the result as an [HttpResponse]. + */ + override fun serve( + ctx: ServiceRequestContext, + req: HttpRequest, + ): HttpResponse { + val userContext = CoroutineContexts.get(ctx) ?: EmptyCoroutineContext + return HttpResponse.of( + CoroutineScope( + ctx.asCoroutineContext() + userContext, + ).future { + suspendedServe(ctx, req) + }, + ) + } + + /** + * Serves an incoming [HttpRequest] in a [CoroutineScope]. + */ + suspend fun suspendedServe( + ctx: ServiceRequestContext, + req: HttpRequest, + ): HttpResponse +} diff --git a/kotlin/src/test/kotlin/com/linecorp/armeria/server/kotlin/CoroutineHttpServiceTest.kt b/kotlin/src/test/kotlin/com/linecorp/armeria/server/kotlin/CoroutineHttpServiceTest.kt new file mode 100644 index 00000000000..d95a580f08c --- /dev/null +++ b/kotlin/src/test/kotlin/com/linecorp/armeria/server/kotlin/CoroutineHttpServiceTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.server.kotlin + +import com.linecorp.armeria.common.HttpResponse +import com.linecorp.armeria.common.HttpStatus +import com.linecorp.armeria.server.ServerBuilder +import com.linecorp.armeria.server.ServiceRequestContext +import com.linecorp.armeria.testing.junit5.server.ServerExtension +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class CoroutineHttpServiceTest { + companion object { + @JvmField + @RegisterExtension + val server = + object : ServerExtension() { + override fun configure(sb: ServerBuilder) { + sb.service( + "/hello", + CoroutineHttpService { ctx, req -> + assertContextPropagation() + HttpResponse.of("hello world") + }, + ).decorator( + CoroutineContextService.newDecorator { ctx -> + CoroutineName("my-coroutine-name") + }, + ) + } + } + + private suspend fun assertContextPropagation() { + assertThat(ServiceRequestContext.currentOrNull()).isNotNull() + assertThat(currentCoroutineContext()[CoroutineName]?.name).isEqualTo("my-coroutine-name") + } + } + + @Test + fun `Should return hello world when call hello coroutine service`() = + runTest { + val response = server.blockingWebClient().get("/hello") + assertThat(response.status()).isEqualTo(HttpStatus.OK) + assertThat(response.contentUtf8()).isEqualTo("hello world") + } +}