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 e3c682dd8e2..8e714eee523 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 53959feab8c..5099f1d6820 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 36138c1f71c..ff05ff5ddcf 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 0e8b31c045d..cad8966568c 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 @@ -294,6 +294,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)) @@ -312,6 +316,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 7f176a56623..3b803ede2ec 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 @@ -16,8 +16,12 @@ import io.ktor.server.testing.* import io.ktor.util.logging.* import kotlinx.coroutines.* import kotlin.coroutines.* +import kotlin.test.* 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 +36,22 @@ 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]. + */ + 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 +59,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 +81,7 @@ actual constructor( return server } - server.stop(1L, 1L) + server.stopSuspend(1L, 1L) } error(lastFailures) @@ -67,7 +92,6 @@ actual constructor( parent: CoroutineContext = EmptyCoroutineContext, module: Application.() -> Unit ): EmbeddedServer { - val _port = this.port val environment = applicationEnvironment { val delegate = KtorSimpleLogger("io.ktor.test") this.log = log ?: object : Logger by delegate { @@ -88,7 +112,10 @@ actual constructor( } return embeddedServer(applicationEngineFactory, properties) { - connector { port = _port } + 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 +128,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) }