From 74aa32622bb78ce8ae07d426a85955d405d94486 Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Tue, 19 Nov 2024 22:18:07 +0200 Subject: [PATCH] KTOR-7359 Implement a suspending version of EmbeddedServer.start and EmbeddedServer.stop (#4481) * Add startSuspend/stopSuspend in EmbeddedServer * Make EngineTestBase work on js/wasmJs --- .../ktor-server-core/api/ktor-server-core.api | 4 ++ .../api/ktor-server-core.klib.api | 2 + .../io/ktor/server/engine/EmbeddedServer.kt | 8 ++++ .../engine/EmbeddedServer.jsAndWasmShared.kt | 20 +++++++-- .../ktor/server/engine/EmbeddedServerJvm.kt | 8 ++++ .../ktor/server/engine/EmbeddedServerNix.kt | 8 ++++ .../base/EngineTestBase.jsAndWasmShared.kt | 42 ++++++++++++++++--- 7 files changed, 83 insertions(+), 9 deletions(-) diff --git a/ktor-server/ktor-server-core/api/ktor-server-core.api b/ktor-server/ktor-server-core/api/ktor-server-core.api index d719c85ddcd..ff48e5ac0a4 100644 --- a/ktor-server/ktor-server-core/api/ktor-server-core.api +++ b/ktor-server/ktor-server-core/api/ktor-server-core.api @@ -590,9 +590,13 @@ public final class io/ktor/server/engine/EmbeddedServer { public final fun reload ()V public final fun start (Z)Lio/ktor/server/engine/EmbeddedServer; public static synthetic fun start$default (Lio/ktor/server/engine/EmbeddedServer;ZILjava/lang/Object;)Lio/ktor/server/engine/EmbeddedServer; + public final fun startSuspend (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun startSuspend$default (Lio/ktor/server/engine/EmbeddedServer;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun stop (JJ)V public final fun stop (JJLjava/util/concurrent/TimeUnit;)V public static synthetic fun stop$default (Lio/ktor/server/engine/EmbeddedServer;JJILjava/lang/Object;)V + public final fun stopSuspend (JJLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun stopSuspend$default (Lio/ktor/server/engine/EmbeddedServer;JJLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class io/ktor/server/engine/EmbeddedServerKt { diff --git a/ktor-server/ktor-server-core/api/ktor-server-core.klib.api b/ktor-server/ktor-server-core/api/ktor-server-core.klib.api index 9348ed21932..343700ea9fb 100644 --- a/ktor-server/ktor-server-core/api/ktor-server-core.klib.api +++ b/ktor-server/ktor-server-core/api/ktor-server-core.klib.api @@ -416,6 +416,8 @@ final class <#A: io.ktor.server.engine/ApplicationEngine, #B: io.ktor.server.eng final fun start(kotlin/Boolean = ...): io.ktor.server.engine/EmbeddedServer<#A, #B> // io.ktor.server.engine/EmbeddedServer.start|start(kotlin.Boolean){}[0] final fun stop(kotlin/Long = ..., kotlin/Long = ...) // io.ktor.server.engine/EmbeddedServer.stop|stop(kotlin.Long;kotlin.Long){}[0] + final suspend fun startSuspend(kotlin/Boolean = ...): io.ktor.server.engine/EmbeddedServer<#A, #B> // io.ktor.server.engine/EmbeddedServer.startSuspend|startSuspend(kotlin.Boolean){}[0] + final suspend fun stopSuspend(kotlin/Long = ..., kotlin/Long = ...) // io.ktor.server.engine/EmbeddedServer.stopSuspend|stopSuspend(kotlin.Long;kotlin.Long){}[0] } final class <#A: kotlin/Any, #B: io.ktor.events/EventDefinition<#A>> io.ktor.server.application.hooks/MonitoringEvent : io.ktor.server.application/Hook> { // io.ktor.server.application.hooks/MonitoringEvent|null[0] diff --git a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/EmbeddedServer.kt b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/EmbeddedServer.kt index 8ff29daec65..05cc99fb901 100644 --- a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/EmbeddedServer.kt +++ b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/EmbeddedServer.kt @@ -6,6 +6,7 @@ package io.ktor.server.engine import io.ktor.events.* import io.ktor.server.application.* +import io.ktor.server.engine.internal.* import io.ktor.util.logging.* import kotlinx.coroutines.* import kotlin.coroutines.* @@ -34,10 +35,17 @@ public expect class EmbeddedServer + public suspend fun startSuspend(wait: Boolean = false): EmbeddedServer + public fun stop( gracePeriodMillis: Long = engineConfig.shutdownGracePeriod, timeoutMillis: Long = engineConfig.shutdownGracePeriod ) + + public suspend fun stopSuspend( + gracePeriodMillis: Long = engineConfig.shutdownGracePeriod, + timeoutMillis: Long = engineConfig.shutdownGracePeriod + ) } /** diff --git a/ktor-server/ktor-server-core/jsAndWasmShared/src/io/ktor/server/engine/EmbeddedServer.jsAndWasmShared.kt b/ktor-server/ktor-server-core/jsAndWasmShared/src/io/ktor/server/engine/EmbeddedServer.jsAndWasmShared.kt index b24fc43895b..b7a8466fbb9 100644 --- a/ktor-server/ktor-server-core/jsAndWasmShared/src/io/ktor/server/engine/EmbeddedServer.jsAndWasmShared.kt +++ b/ktor-server/ktor-server-core/jsAndWasmShared/src/io/ktor/server/engine/EmbeddedServer.jsAndWasmShared.kt @@ -44,9 +44,7 @@ actual constructor( private val modules = rootConfig.modules - public actual fun start(wait: Boolean): EmbeddedServer { - addShutdownHook { stop() } - + private fun prepareToStart() { safeRaiseEvent(ApplicationStarting, application) try { modules.forEach { application.it() } @@ -65,9 +63,20 @@ actual constructor( ) } } + } + public actual fun start(wait: Boolean): EmbeddedServer { + addShutdownHook { stop() } + prepareToStart() engine.start(wait) + return this + } + @OptIn(DelicateCoroutinesApi::class) + public actual suspend fun startSuspend(wait: Boolean): EmbeddedServer { + addShutdownHook { GlobalScope.launch { stopSuspend() } } + prepareToStart() + engine.startSuspend(wait) return this } @@ -76,6 +85,11 @@ actual constructor( destroy(application) } + public actual suspend fun stopSuspend(gracePeriodMillis: Long, timeoutMillis: Long) { + engine.stopSuspend(gracePeriodMillis, timeoutMillis) + destroy(application) + } + private fun destroy(application: Application) { safeRaiseEvent(ApplicationStopping, application) try { diff --git a/ktor-server/ktor-server-core/jvm/src/io/ktor/server/engine/EmbeddedServerJvm.kt b/ktor-server/ktor-server-core/jvm/src/io/ktor/server/engine/EmbeddedServerJvm.kt index 477c55764b0..aec6793f1b5 100644 --- a/ktor-server/ktor-server-core/jvm/src/io/ktor/server/engine/EmbeddedServerJvm.kt +++ b/ktor-server/ktor-server-core/jvm/src/io/ktor/server/engine/EmbeddedServerJvm.kt @@ -295,6 +295,10 @@ actual constructor( return this } + public actual suspend fun startSuspend(wait: Boolean): EmbeddedServer { + return withContext(Dispatchers.IOBridge) { start(wait) } + } + public fun stop(shutdownGracePeriod: Long, shutdownTimeout: Long, timeUnit: TimeUnit) { try { engine.stop(timeUnit.toMillis(shutdownGracePeriod), timeUnit.toMillis(shutdownTimeout)) @@ -313,6 +317,10 @@ actual constructor( stop(gracePeriodMillis, timeoutMillis, TimeUnit.MILLISECONDS) } + public actual suspend fun stopSuspend(gracePeriodMillis: Long, timeoutMillis: Long) { + withContext(Dispatchers.IOBridge) { stop(gracePeriodMillis, timeoutMillis) } + } + private fun instantiateAndConfigureApplication(currentClassLoader: ClassLoader): Application { val newInstance = if (recreateInstance || applicationInstance == null) { Application( diff --git a/ktor-server/ktor-server-core/posix/src/io/ktor/server/engine/EmbeddedServerNix.kt b/ktor-server/ktor-server-core/posix/src/io/ktor/server/engine/EmbeddedServerNix.kt index 9720830df6f..f9c1bdedec3 100644 --- a/ktor-server/ktor-server-core/posix/src/io/ktor/server/engine/EmbeddedServerNix.kt +++ b/ktor-server/ktor-server-core/posix/src/io/ktor/server/engine/EmbeddedServerNix.kt @@ -71,11 +71,19 @@ actual constructor( return this } + public actual suspend fun startSuspend(wait: Boolean): EmbeddedServer { + return withContext(Dispatchers.IOBridge) { start(wait) } + } + public actual fun stop(gracePeriodMillis: Long, timeoutMillis: Long) { engine.stop(gracePeriodMillis, timeoutMillis) destroy(application) } + public actual suspend fun stopSuspend(gracePeriodMillis: Long, timeoutMillis: Long) { + withContext(Dispatchers.IOBridge) { stop(gracePeriodMillis, timeoutMillis) } + } + private fun destroy(application: Application) { safeRaiseEvent(ApplicationStopping, application) try { diff --git a/ktor-server/ktor-server-test-base/jsAndWasmShared/src/io/ktor/server/test/base/EngineTestBase.jsAndWasmShared.kt b/ktor-server/ktor-server-test-base/jsAndWasmShared/src/io/ktor/server/test/base/EngineTestBase.jsAndWasmShared.kt index 1c6d68b0d96..ccbbf54f386 100644 --- a/ktor-server/ktor-server-test-base/jsAndWasmShared/src/io/ktor/server/test/base/EngineTestBase.jsAndWasmShared.kt +++ b/ktor-server/ktor-server-test-base/jsAndWasmShared/src/io/ktor/server/test/base/EngineTestBase.jsAndWasmShared.kt @@ -15,9 +15,14 @@ import io.ktor.server.routing.* import io.ktor.server.testing.* import io.ktor.util.logging.* import kotlinx.coroutines.* -import kotlin.coroutines.* +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.test.AfterTest import kotlin.time.Duration.Companion.seconds +private const val UNINITIALIZED_PORT = -1 +private const val DEFAULT_PORT = 0 + actual abstract class EngineTestBase< TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration @@ -32,7 +37,23 @@ actual constructor( @Retention protected actual annotation class Http2Only actual constructor() - protected actual var port: Int = 0 + /** + * It's not possible to find a free port during test setup, + * as on JS (Node.js) all APIs are non-blocking (suspend). + * That's why we assign port after the server is started in [startServer] + * Note: this means, that [port] can be used only after calling [createAndStartServer] or [startServer]. + */ + @Suppress("ktlint:standard:backing-property-naming") + private var _port: Int = UNINITIALIZED_PORT + protected actual var port: Int + get() { + check(_port != UNINITIALIZED_PORT) { "Port is not initialized" } + return _port + } + set(_) { + error("Can't reassign port.") + } + protected actual var sslPort: Int = 0 protected actual var server: EmbeddedServer? = null @@ -40,6 +61,12 @@ actual constructor( protected actual var enableSsl: Boolean = false protected actual var enableCertVerify: Boolean = false + @OptIn(DelicateCoroutinesApi::class) + @AfterTest + fun tearDownBase() { + GlobalScope.launch { server?.stopSuspend(gracePeriodMillis = 0, timeoutMillis = 500) } + } + protected actual suspend fun createAndStartServer( log: Logger?, parent: CoroutineContext, @@ -56,7 +83,7 @@ actual constructor( return server } - server.stop(1L, 1L) + server.stopSuspend(1L, 1L) } error(lastFailures) @@ -67,7 +94,6 @@ actual constructor( parent: CoroutineContext = EmptyCoroutineContext, module: Application.() -> Unit ): EmbeddedServer { - val savedPort = this.port val environment = applicationEnvironment { val delegate = KtorSimpleLogger("io.ktor.test") this.log = log ?: object : Logger by delegate { @@ -88,7 +114,10 @@ actual constructor( } return embeddedServer(applicationEngineFactory, properties) { - connector { port = savedPort } + connector { + // the default port is zero, so that it will be automatically assigned when the server is started. + port = DEFAULT_PORT + } shutdownGracePeriod = 1000 shutdownTimeout = 1000 } @@ -101,7 +130,8 @@ actual constructor( // we start it on the global scope because we don't want it to fail the whole test // as far as we have retry loop on call side val starting = GlobalScope.async { - server.start(wait = false) + server.startSuspend(wait = false) + _port = server.engine.resolvedConnectors().first().port delay(500) }